# LASSO Regresion on covid-19 cases in NYS

## Load modulues

```python
%matplotlib inline
import os
import requests
import urllib

import pandas as pd
import numpy as np
import statsmodels.api as sm
import scipy

import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from statsmodels.sandbox.regression.predstd import wls_prediction_std
from sklearn import linear_model
from scipy import stats as sps
from sklearn.feature_selection import SelectFromModel
from IPython.display import display

locator = mdates.AutoDateLocator()
formatter = mdates.ConciseDateFormatter(locator)

sns.set_style("whitegrid")
GAMMA = 1/7.5
current_palette = sns.color_palette()
```

## Important dates

```python
# interventin dates
# 03/10/2020 school close in New Rochelle
# https://www.governor.ny.gov/news/during-novel-coronavirus-briefing-governor-cuomo-announces-new-mass-gatherings-regulations
# 03/12/2020 mass gathering reduced to 500 max. 50% occupancy

# 03/17/2020 close of gyms, restaurants and bars, movie theaters, mass gathering up to 50. https://www.governor.ny.gov/news/amid-lack-federal-direction-governor-cuomo-governor-murphy-and-governor-lamont-announce
bars = pd.to_datetime('03-16-2020 20:00', dayfirst=False)
# 03/18/2020 school clousure http://www.nysed.gov/news/2020/state-education-department-issues-updated-guidance-schools-regarding-novel-coronavirus
schools = pd.to_datetime('03-18-2020 20:00', dayfirst=False)

# https://www.governor.ny.gov/news/amid-ongoing-covid-19-pandemic-governor-cuomo-announces-deployment-1000-bed-hospital-ship-usns
# 03/20/2020 00:00 50% of the workforce
workforce_50 = pd.to_datetime('03-20-2020 20:00', dayfirst=False)
# 03/22/2020 20:00 ny_pause
ny_pause = pd.to_datetime('22-03-2020 00:00', dayfirst=True)
# CDC masks https://www.npr.org/sections/goatsandsoda/2020/04/10/829890635/why-there-so-many-different-guidelines-for-face-masks-for-the-public
masks_cdc = pd.to_datetime('03-04-2020 00:00', dayfirst=True)
mask_employers = pd.to_datetime('12-04-2020 00:00', dayfirst=True)
mask_public = pd.to_datetime('17-04-2020 00:00', dayfirst=True)

events = [bars, schools, workforce_50, ny_pause, masks_cdc, mask_employers, mask_public]
```

## Get data

```python
dsname = 'New York'

# API information
# https://health.data.ny.gov/Health/New-York-State-Statewide-COVID-19-Testing/xdss-u53e
force_update = True
if not os.path.isfile('data/ny.csv') or force_update:
    url = urllib.parse.quote("https://health.data.ny.gov/resource/xdss-u53e.csv/?$limit=5000")
    #
    dfs = []
    offset = 0
    while offset >=0:
        url = "https://health.data.ny.gov/resource/xdss-u53e.csv/?$limit=5000&$offset={}".format(offset)
        df = pd.read_csv(url, usecols=['test_date', 'total_number_of_tests', 'new_positives'])
        dfs.append(df)
        if len(df)==5000:
            offset += 5000
        else:
            offset = -1
    dfraw = pd.concat(dfs)
    #'test_date=2020-03-15T00:00:00.000'

    dfraw = dfraw.rename(columns={'new_positives': 'Positives', 'total_number_of_tests': 'Tests', 'test_date': 'date'})
    print(len(dfraw))
    dfraw['date'] = pd.to_datetime(dfraw['date'])
    #counties = (df.groupby('date')['Tests']>0).count()
    df = dfraw.groupby('date').sum()
    print(df.head(1))
    df['Odds'] = df.Positives / (df.Tests - df.Positives)
    df['Date'] = pd.to_datetime(df.index)
    df.to_csv('data/ny.csv')
    df = df[df['Date'] >= '2020-03-15']
    # last date of full NYS PAUSE
    df = df[df['Date'] <= '2020-05-15']

else:
    df = pd.read_csv('data/ny.csv')
    df['date'] =pd.to_datetime(df['date'])
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('date')
    df = df[df['Date'] >= '2020-03-15']
    # last date of full NYS PAUSE
    df = df[df['Date'] <= '2020-05-15']
df = df[['Date', 'Tests', 'Positives', 'Odds']]

df.head()
```

    6324
                Positives  Tests
    date
    2020-03-02          1      1

      Date
      Tests
      Positives
      Odds

      date

      2020-03-15
      2020-03-15
      1936
      294
      0.179050

      2020-03-16
      2020-03-16
      2907
      432
      0.174545

      2020-03-17
      2020-03-17
      4553
      1009
      0.284707

      2020-03-18
      2020-03-18
      7698
      1769
      0.298364

      2020-03-19
      2020-03-19
      10124
      2950
      0.411207

## Plots

I we plot the number of positive tests we can see that the data is noisy.
But, if we take into account the number of people tested each day, the data looks way more clean.

```python
plt.close('all')
ax = df.plot(y=['Positives', 'Tests'], secondary_y=['Tests'], legend=True)
plt.vlines(events, 0, df.Tests.max(), linestyle='--')
plt.tight_layout()
plt.savefig('figs/tests_and_cases.jpg', dpi=300)
plt.show()

plt.figure()
ax = sns.scatterplot(data=df, x='Date', y='Odds')
ax.set_yscale('log')
ax.set_xlim([df.Date.min(), df.Date.max()])
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)

```

## Relationship between the total number of infected individuals and positive tests

As has been shown previously [[1]](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0002185), the number of new infected individuals in a given day $k_t$ is given by:
$$
k_t = k_{t-1} e^{(R_{t-1}-1)\gamma}
$$

where $R_t$ is the effective reproductuve number and $\gamma^{-1}$ is the infectious period estimated as 7 days accoring to [2].

The following derivation was suggested to my by Will Meierjurgen Farr on this GitHub [Issue](https://github.com/k-sys/covid-19/issues/45#issuecomment-623782130).
Since we do not have access to the total number of infected indiviudals, but only to the population being tested, we have to use some statisticals assumtions on this populations.
If we asume that the people being tested, in a given day, is a sample of the population with COVID-19-like sympoms we can state that:

$$
n_{t} = [P_t(CV|sympoms) P_t(sympoms) +P_t(not CV|sympoms)P_t(sympoms)]Nf_t
$$

where $n_{t}$ is the number of people tested, $P_t(CV|sympoms)$ is the probablity of a pacient being positive for coronavirus given that the she is sympomatic, $P_t(sympoms)$ is the probablity of having COVID-like sympoms, $P_t(not CV|sympoms)$ is the probability of a pacient being coronavirus negative given he has COVID-19-like sympoms, $N$ is the total population, and $f_t$ is the fraction of people with sympoms that is selected to be tested (this number can be different each day, for example if the number of tests availabes changes).
Also, note that the probability of a test being positive in a given day is $Positive_t=P_t(CV|sympoms) P_t(sympoms) N f_t$

Now, if we assume that $P_t(sympoms|CV)=cte$ we can use Bayes theorem to show that:

$$
P_t(CV|sympoms) P_t(sympoms) \propto P_t(CV) = \frac{k_t}{N}
$$

Then:
$$
P_t(CV|sympoms) P_t(sympoms) \propto k_t
$$

Finally, if we assume that $P_t(not CV|sympoms)P_t(sympoms)=cte$:
$$
Odds_t = \frac{P(CV|sympoms) P(sympoms)Nf_t}{P_t(not CV|sympoms)P_t(sympoms)Nf_t}
$$
$$
Odds_t = \frac{P(CV|sympoms) P(sympoms)}{P_t(not CV|sympoms)P_t(sympoms)}
$$
$$
Odds_t \propto k_t
$$

\begin{equation}
Odds_t = Odds_{t-1} e^{(R_{t-1}-1)\gamma} (1)
\end{equation}

We used three hypothesis. First, constant population $N$ (for $P_t \propto k_t$ and for the evolution of $k_t$). Second, that the tested population is a random sample from the population with COVID19-like sympoms ($n_t = [P_t(CV|sympoms) P_t(sympoms) +P_t(not CV|sympoms)P_t(sympoms)]Nf_t$) this is not the case when people is being tested based on contacts for example. And third, that $P_t(not CV|sympoms)P_t(sympoms)=cte$, this is equivalent to say that the number of people with covid-19-like sympoms but without the coronavirus (for example people with the flu) is constant, or at least it changes are negligible compared with the changes in the amount of sympomatic people with coronavirus.

## Linearization

Defining

$$
b_i =  e^{(R_{i-1}-1)\gamma} (2)
$$

We can write [1] as:

\begin{equation}
odd_i = b_{i-1} * odd_{i-1} (3)
\end{equation}

Now, instead of using $b_i$ as the parameters to estimate we decompose each $b$ as follows:

$$
b_i = \prod_{j=0}^{i} a_j (4)
$$

Now, the $a_j$ represent the rate of change of the variable $b_i$. Next, we replace the [4] in [3]
$$
odd_i = \prod_{j=0}^{i-1} a_j * odd_{i-1}
$$
$$
odd_i = \prod_{j=0}^{i-1} a_j * \prod_{j=0}^{i-2} a_j * odd_{i-2}
$$
$$
odd_i = \prod_{k=0}^{i-1}\prod_{j=0}^{k} a_j * odd_{0}
$$
$$
odd_i = \prod_{j=0}^{i-1} a_j^{i-j} * odd_{0}
$$

Now, we liniarize this result and we generalize it to the case where $i=0$ using the $max$ function:

$$
log(odd_i) = \sum_{j=0}^{max(i-1, 0)} (i-j)log(a_j)  +  log(odd_{0}) (5)
$$

We can write [5] as a linear problem with the following definitions:

$$
y = X \beta + \beta_0
$$

$$
y_i = log(odd_i)
$$

$$
X_{i,j} =  max(i-j, 0)
$$

$$
\beta_i =  log(a_i) (6)
$$

Now if we apply a LASSO regression we will find the solution that minimize the following cost function

$$
Err = \sum (y-\hat{y})^2 + \alpha |\beta|_1
$$

Hopefully, this solution will be sparse which means that most of the $\beta_i$ will be $0$, and hence $a_i=1$.
This is equivalent to say that the $b_i$ are almost constant except at the values whete $a_i \neq 1$.

[1] Bettencourt, L. M. A., & Ribeiro, R. M. (2008). Real time bayesian estimation of the epidemic potential of emerging infectious diseases. PLoS ONE, 3(5). https://doi.org/10.1371/journal.pone.0002185

[2] Sanche, S., Lin, Y. T., Xu, C., Romero-Severson, E., Hengartner, N., & Ke, R. (2020). High Contagiousness and Rapid Spread of Severe Acute Respiratory Syndrome Coronavirus 2. Emerging Infectious Diseases, 26(7). https://doi.org/10.3201/eid2607.200282

## Classes

This cell contains the main class: LassoICSelector. Its main method is fit_best_alpha. It works as follows:
```
For each alpha value:
    1. Fits a lasso regression to the data
    2. Selectes the first non zero variable from each chunck
    3. Fits a linear regression with the selected variables
    4. Excludes all non sifgificative (p-value>0.05) variables and fits a linear model again

The linear model with less AIC from step 4 is selected.
```

```python
class FirstInChunkSelector(object):
    '''Selects first element from each non zero chunk.'''

    def __init__(self, clf):
        self.clf = clf
        self.coef = None
        self.mask = None

    def select_coef(self):
        n_features = len(self.clf.coef_)
        no_zero = np.zeros(n_features+1)
        no_zero[1:] = self.clf.coef_ != 0
        self.mask = np.diff(no_zero)>0
        self.coef = self.clf.coef_[self.mask]
        return self.coef

    def transform(self, X):
        self.select_coef()
        return X[:, self.mask]

    def get_support(self):
        self.select_coef()
        return self.mask

    def get_number_of_features(self):
        self.select_coef()
        return sum(self.mask)

class LassoICSelector(object):
    """LASSO regression with FirstInChunk selector."""

    def __init__(self, X, y, criterion, alpha=0.05):
        self.lasso = linear_model.LassoLars(alpha=0, max_iter=100000)
        self.criterion = criterion
        self.selector = FirstInChunkSelector(self.lasso)
        self.OLS = sm.OLS
        self.ols = self.OLS(y, X)
        self.ols_results = None
        self.X = X
        self.y = y
        self.final_ols = False
        self.alpha = alpha

    def transform_to_ols(self, X):
        '''Selects only the features of X are used by OLS.
        Also, adds a coloumn with ones for the intercept.
        '''

        X_new = self.selector.transform(X)
        if self.final_ols:
            X_new = X[:, self.support]
        X_new_with_cte = np.hstack([X_new, np.ones((X_new.shape[0], 1))])
        return X_new_with_cte

    def fit(self, X, y):
        '''Selects features and fits the OLS.'''

        # select features
        X_new = self.transform_to_ols(X)

        # fit ols
        self.ols = self.OLS(y, X_new)
        self.ols_results = self.ols.fit()

        # iteratively remove non signicative variables and fit again
        mask = self.ols_results.pvalues < self.alpha / len(self.ols_results.pvalues)
        Xnew = self.transform_to_ols(X)
        Xnew = Xnew[:, mask]
        self.support = self.selector.get_support()
        self.ols = self.OLS(y, Xnew)
        self.ols_results = self.ols.fit()
        while any(self.ols_results.pvalues >= self.alpha / len(self.ols_results.pvalues)):
            mask.values[mask.values] = (self.ols_results.pvalues < self.alpha / len(self.ols_results.pvalues)).values
            Xnew = self.transform_to_ols(X)
            Xnew = Xnew[:, mask]
            self.support = self.selector.get_support()
            self.ols = self.OLS(y, Xnew)
            self.ols_results = self.ols.fit()

        self.support[self.support] = mask[:-1]

    def fit_best_alpha(self, X, y):
        '''returns the model with the lowst cirterion.'''

        self.lasso.fit(X, y)
        alphas = self.lasso.alphas_
        self.criterions_ = np.zeros(len(alphas))
        self.log_liklehods = np.zeros(len(alphas))

        for i, alpha in enumerate(alphas):
            self.lasso.coef_ = self.lasso.coef_path_[:, i]
            self.fit(X, y)
            self.criterions_[i], self.log_liklehods[i] = self.get_criterion(self.ols.exog, y)

        # we use a list of tuples to find the minimum cirterion value.
        # If there are ties, we use the maximum alpha value.
        criterions_idx = list(zip(self.criterions_, alphas, range(len(alphas))))
        criterion, alpha, idx = min(criterions_idx, key=lambda x: (x[0], -x[1]))
        #print(list(enumerate(zip(alphas, self.criterions_))))
        #print(alpha, criterion)
        self.lasso.coef_ = self.lasso.coef_path_[:, idx]
        self.lasso.alpha = alpha
        self.fit(X, y)
        self.final_ols = True

    def predict(self, X):
        '''Predicts y useing the OLS fit.'''

        return self.ols.predict(self.ols_results.params, X)

    def log_liklihood(self, X, y):
        '''Computes the log liklihood assuming normally distributed errors.'''

        eps64 = np.finfo('float64').eps

        # residuals
        R = y - self.predict(X)
        sigma2 = np.var(R)

        loglike = -0.5 * len(R) * np.log(sigma2)
        loglike -= 0.5 * len(R) * np.log(2*np.pi) - 0.5*len(R) + 0.5
        return loglike

    def get_criterion(self, X, y):
        '''Computes AIC or BIC criterion.'''

        n_samples = X.shape[0]
        if self.criterion == 'aic':
            K = 2  # AIC
        elif self.criterion == 'bic':
            K = np.log(n_samples)
        else:
            raise ValueError('criterion should be either bic or aic')

        log_like = self.log_liklihood(X, y)
        df = X.shape[1]

        aic = K * df - 2*log_like
        self.criterion_ = aic

        return self.criterion_, log_like
```

## Fit
Now, we create the linear system and fit the model

```python
# create the independent and the dependent variables
y = np.log(df['Odds'])
X = np.tri(len(y))
X = np.cumsum(X, axis=0)[:, 1:]

# create lasso instance
lics = LassoICSelector(X, y.values, 'bic')

# fit
lics.fit_best_alpha(X, y)

plt.close('all')

plt.figure()
plt.plot(lics.lasso.alphas_, lics.criterions_)
plt.scatter(lics.lasso.alphas_, lics.criterions_)
plt.vlines(lics.lasso.alpha, min(lics.criterions_), max(lics.criterions_))
plt.title('alpha={}'.format(lics.lasso.alpha))
plt.ylabel('AIC')
plt.xlabel('Alpha')
plt.xscale('log')

lics.ols_results.summary()
```

Lets copy the fitted values to a dataframe, and calculate the parameters and erros of the model.

```python
data = df.copy()

yhat = lics.ols_results.fittedvalues

# from equation 5
odds_hat = np.exp(yhat)

# the error in yhat is
Xols = lics.transform_to_ols(X)
(yhat_std, yhat_l, yhat_u) = wls_prediction_std(lics.ols_results, Xols)

# propagation of errors
oddshat_std = odds_hat*yhat_std

data.loc[:, 'odds_hat'] = odds_hat
data.loc[:, 'oddshat_std'] = oddshat_std
data.loc[:, 'oddshat_l'] = odds_hat - 2*oddshat_std
data.loc[:, 'oddshat_u'] = odds_hat + 2*oddshat_std

# use coefficients to calculate Rt
coef = np.zeros(len(data))
coef_std = np.zeros_like(coef) * np.nan
ind = np.squeeze(np.argwhere(lics.support))

# we do not use the last coefficient since it's the intercept (=log(odds_0))
coef[ind] = lics.ols_results.params[:-1]

# using equation 2, 4 and 6
data.loc[:, 'R'] = np.cumsum(coef)/GAMMA+1

# get covarinace matrix of coefficients
cov = lics.ols_results.cov_params().values

# since the values of Rts are a sum of variables, we use the formula
# of the sum of gaussian variables with a known covariance matrix
stds = [np.sqrt(cov[:n, :n].sum()) for n in range(1, cov.shape[0])]

coef_std[ind] = stds

# error propagation formula
data.loc[:, 'Rstd'] = coef_std / GAMMA

data['Rstd'] = data['Rstd'].fillna(method='ffill')
data['R_l'] = data['R'] - 2*data['Rstd']
data['R_u'] = data['R'] + 2*data['Rstd']

r_index = data.R.diff() != 0
Rts = data.loc[r_index, ['R', 'R_l', 'R_u']]
display(Rts)
```

      R
      R_l
      R_u

      date

      2020-03-15
      2.097828
      1.943739
      2.251917

      2020-03-23
      1.567515
      1.431863
      1.703166

      2020-03-30
      0.726109
      0.675574
      0.776644

      2020-04-14
      0.439509
      0.415892
      0.463126

Now, we plot the Rt as function of time and the fitted values.

```python
plt.close('all')

ax = data.plot(x='Date', y='R', legend=False)
ax.fill_between(data.index, data['R_u'], data['R_l'],
                facecolor='blue', alpha=0.2, label='95% CI')
ax.vlines(events, 0, data.R_u.max(), linestyle='--')

plt.ylabel('Rt')
plt.tight_layout()
plt.savefig('figs/RtL1.jpg', dpi=300)
plt.show()

plt.figure()
ax = sns.scatterplot(x='Date', y='Odds', data=data, label='Data')
ax = sns.lineplot(x='Date', y='odds_hat', label='Fit', ax=ax, data=data)
ax.fill_between(data.index, data['oddshat_l'],
                data['oddshat_u'],
                facecolor='blue', alpha=0.1, label='95% CI')

ax.legend()
ax.set_yscale('log')

ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)

plt.ylabel('Odds')
plt.tight_layout()
ax.set_xlim(data['Date'].min(), data['Date'].max())
plt.savefig('figs/OddsL1.jpg', dpi=300)
plt.show()
```

## Counterfactual (what if)

1. What if the final step in Rt had not happened

```python
fitted_params = lics.ols_results.params.copy()
current_palette = sns.color_palette()

i = 3

params_cf = lics.ols_results.params.copy()
params_cf[i] = 0
lics.ols_results.params = params_cf
yhat = lics.predict(lics.transform_to_ols(X))
odds_cf = np.exp(yhat)
(yhat_std, yhat_l, yhat_u) = wls_prediction_std(lics.ols_results, lics.transform_to_ols(X))
oddshat_std = odds_cf*yhat_std

lics.ols_results.params = fitted_params
ratio_cf = odds_cf / (odds_cf+1)

coef = np.zeros(len(data))
ind = np.squeeze(np.argwhere(lics.support))
coef[ind] = params_cf[:-1]
data['R_cf_{}'.format(i)] = np.cumsum(coef)/GAMMA+1

data['Odds CF {}'.format(i)] = odds_cf
data['Odds CF {} std'.format(i)] = oddshat_std
data['oddshat_l_cf_{}'.format(i)] = odds_cf - 2*oddshat_std
data['oddshat_u_cf_{}'.format(i)] = odds_cf + 2*oddshat_std

data['ratio_cf_{}'.format(i)] = ratio_cf
```

Lets plot the counterfacutal odds

```python
plt.figure()
i = 3
ax = sns.scatterplot(x='Date', y='Odds', data=data, label='Data', c=np.array([current_palette[0]]))

ax = sns.lineplot(x='Date', y='odds_hat', data=data, ax=ax, palette=[current_palette[i]], label='Fit')
ax.fill_between(data.index, data['oddshat_l'],
                data['oddshat_u'],
                alpha=0.1)

ax = sns.lineplot(x='Date', y='Odds CF {}'.format(i), data=data, ax=ax, palette=[current_palette[i]], label='Counterfactual No Masks')
ax.fill_between(data.index, data['oddshat_l_cf_{}'.format(i)],
                data['oddshat_u_cf_{}'.format(i)],
                alpha=0.1)

plt.yscale('log')

ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
plt.grid(True)
plt.ylabel('Odds')
ax.set_xlim(data['Date'].min(), data.loc[data.odds_hat.notna(), 'Date'].max())
plt.tight_layout()
plt.savefig('figs/odds_cf_masks.jpg', dpi=300)
plt.show()
```

Now, we can calculate the counterfactual number of cases

```python
data[['Date', 'Positives', 'Tests', 'Odds', 'Odds CF 3']]
Positives_CF_3 = data['Tests'] * data['Odds CF 3']/(data['Odds CF 3']+1)
Positives_CF_3_std = data['Odds CF 3 std'] * data['Tests'] /(data['Odds CF 3']+1)**2
Positives_CF_3_sum_std = np.sqrt((Positives_CF_3_std**2).sum())
Positive_diff = (Positives_CF_3 - data['Positives']).sum()
Positive_diff_l = Positive_diff - 2* Positives_CF_3_sum_std
Positive_diff_u = Positive_diff + 2* Positives_CF_3_sum_std
Positives_fit = data['Tests'] * data['odds_hat']/(data['odds_hat']+1)
print('Counterfactual cases diff {} CI ({} {})'.format(Positive_diff, Positive_diff_l, Positive_diff_u))
print('Counterfactual death diff {} CI ({} {})'.format(0.077*Positive_diff, 0.077*Positive_diff_l, 0.077*Positive_diff_u))
```

    Counterfactual cases diff 70948.16745793185 CI (63796.08804496976 78100.24687089394)
    Counterfactual death diff 5463.008894260753 CI (4912.2987794626715 6013.719009058834)

### All Counterfactual Scenarios

```python
fitted_params = lics.ols_results.params.copy()
current_palette = sns.color_palette()
for i in range(1, 4):
    params_cf = lics.ols_results.params.copy()
    params_cf[i] = 0
    lics.ols_results.params = params_cf
    yhat = lics.predict(lics.transform_to_ols(X))
    odds_cf = np.exp(yhat)
    (yhat_std, yhat_l, yhat_u) = wls_prediction_std(lics.ols_results, lics.transform_to_ols(X))
    oddshat_std = odds_cf*yhat_std

    lics.ols_results.params = fitted_params
    ratio_cf = odds_cf / (odds_cf+1)

    coef = np.zeros(len(data))
    ind = np.squeeze(np.argwhere(lics.support))
    coef[ind] = params_cf[:-1]
    data['R_cf_{}'.format(i)] = np.cumsum(coef)/GAMMA+1

    data['Odds CF {}'.format(i)] = odds_cf

    data['oddshat_l_cf_{}'.format(i)] = odds_cf - 2*oddshat_std
    data['oddshat_u_cf_{}'.format(i)] = odds_cf + 2*oddshat_std

    data['ratio_cf_{}'.format(i)] = ratio_cf

plt.figure()
ax = sns.scatterplot(x='Date', y='Odds', data=data, label='Data', c=np.array([current_palette[0]]))
for i in range(1, 4):
    print(i, current_palette[i])
    ax = sns.lineplot(x='Date', y='Odds CF {}'.format(i), data=data, ax=ax, palette=[current_palette[i]], label='Odds CF {}'.format(i))
    print(i, current_palette[i])
    ax.fill_between(data.index, data['oddshat_l_cf_{}'.format(i)],
                    data['oddshat_u_cf_{}'.format(i)],
                    alpha=0.1)

plt.yscale('log')
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
plt.grid(True)
plt.ylabel('Odds')
ax.set_xlim(data['Date'].min(), data.loc[data.odds_hat.notna(), 'Date'].max())
plt.tight_layout()
plt.savefig('figs/odds_cf.jpg')
plt.show()

data.plot(y=['R', 'R_cf_1', 'R_cf_2', 'R_cf_3'], grid=True)
plt.savefig('figs/R_cf.png')
plt.tight_layout()

```

## Extrapolation
If everything continues as before (it never does), what will happen?

```python
# number of days to extrapolate into the future
n = 22

Xf = np.repeat([np.arange(1, X.shape[1]+1)[::-1]], n, axis=0)
Xf += np.arange(n).reshape(n, 1)
Xf = lics.transform_to_ols(Xf)
yhat_extrapolation = lics.ols.predict(lics.ols_results.params, Xf)
odds_hat_extrapolation = np.exp(yhat_extrapolation)
phat_extrapolation = odds_hat_extrapolation / (odds_hat_extrapolation + 1)

(yhat_std, yhat_l, yhat_u) = wls_prediction_std(lics.ols_results, Xf)
oddshat_std = odds_hat_extrapolation*yhat_std
phat_std = oddshat_std/(1+odds_hat_extrapolation)**2
extrapolation = pd.DataFrame({'oddshat_extrapolation_std': oddshat_std,
                              'odds_hat_extrapolation': odds_hat_extrapolation,
                              'phat_extrapolation': phat_extrapolation,
                              'phat_extrapolation_std': phat_std,
                              'phat_extrapolation_l': phat_extrapolation - 2*phat_std,
                              'phat_extrapolation_u': phat_extrapolation + 2*phat_std
                              },
                             index=data.index.shift(n-1, freq='D')[-n:])
extrapolation = pd.merge(data, extrapolation, how='outer', left_index=True,
                right_index=True)
extrapolation['Date'] = extrapolation.index

extrapolation['p'] = extrapolation['Odds'] / (extrapolation['Odds']+1)
extrapolation['phat'] = extrapolation['odds_hat'] / (extrapolation['odds_hat']+1)
extrapolation['phat_std'] = extrapolation['oddshat_std'] / (extrapolation['odds_hat']+1)**2
extrapolation['phat_l'] = extrapolation['phat'] - 2*extrapolation['phat_std']
extrapolation['phat_u'] = extrapolation['phat'] + 2*extrapolation['phat_std']
extrapolation['extrapolation'] = extrapolation['phat'].isna()

plt.close('all')
plt.figure()
ax = sns.scatterplot(data=extrapolation, x='Date', y='p', label='Data')
ax = sns.lineplot(data=extrapolation, x='Date', y='phat', ax=ax, label='Fit')

ax.fill_between(extrapolation.index, extrapolation['phat_l'],
                extrapolation['phat_u'],
                alpha=0.1, color=current_palette[0])

ax = sns.lineplot(data=extrapolation, x='Date', y='phat_extrapolation', ax=ax, label='Extrapolation')
ax.fill_between(extrapolation.index, extrapolation['phat_extrapolation_l'],
                extrapolation['phat_extrapolation_u'],
                alpha=0.1, color=current_palette[1])
ax.set_xlim([extrapolation.index.min(), extrapolation.index.max()])
ax.set_ylabel('P(Positive)');
#ax.set_yscale('log')
```


```python
import matplotlib.pyplot as plt
import numpy as np
import math

x = np.arange(0.0, 2.0, 0.01)
y = sin(z)-cos(x)
e = math.e
z= e**(x)

plt.plot(x, y)

plt.title('F(X)')
plt.xlabel('X')
plt.ylabel('Y')
plt.grid(True)
plt.legend(['y = f(x)=sin(e^(x)-cos(x)'], loc='upper left')

plt.show()
```

```python
from sympy.solvers import solve
from sympy import Symbol
x = Symbol('x')
y = Symbol('y')
x = 7
q = x + y - 25

solve(q,y)
print('Y =',solve(q,y),'X =',x)
```

    Y = [18] X = 7

```python
a = np.array([[3,0,0],[1,8,0],[0,4,-2]])
b = np.array([30,18,2])
x = np.linalg.solve(a, b)
print(x)
y = array(1) + array(2) + 3*array(3)
print('Coco + Manzana + 3 Platanos = ',y)
```

    [10.  1.  1.]
    Coco + Manzana + 3 Platanos =  12

```python
import string

message = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq   ypc " \
          "dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq " \
          "rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu " \
          "ynnjw ml rfc spj."

table = str.maketrans(string.ascii_lowercase, string.ascii_lowercase[2:] + string.ascii_lowercase[:2])

print(message.translate(table))
```

    i hope you didnt translate it by hand. thats what computers   are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url.

```python
!pip install pulp
```

    Requirement already satisfied: pulp in ./anaconda3/lib/python3.7/site-packages (1.6.9)
    Requirement already satisfied: pyparsing>=2.0.1 in ./anaconda3/lib/python3.7/site-packages (from pulp) (2.3.0)

```python
from pulp import *
pulp.LpVariable?

mi_lp_problema = pulp.LpProblem('', pulp.LpMinimize)

x11 = pulp.LpVariable('x11', lowBound = 0, cat = 'Continuous')
x12 = pulp.LpVariable('x12', lowBound = 0, cat = 'Continuous')
x13 = pulp.LpVariable('x13', lowBound = 0, cat = 'Continuous')
x21 = pulp.LpVariable('x21', lowBound = 0, cat = 'Continuous')
x22 = pulp.LpVariable('x22', lowBound = 0, cat = 'Continuous')
x23 = pulp.LpVariable('x23', lowBound = 0, cat = 'Continuous')

mi_lp_problema += 8*x11 + 6*x12 + 10*x13 + 10*x21 + 4*x22 + 9*x23

mi_lp_problema += x11 + x12 + x13 == 2000
mi_lp_problema += x21 + x22 + x23 == 2500
mi_lp_problema += x11 + x21 == 1500
mi_lp_problema += x12 + x22 == 2000
mi_lp_problema += x13 + x23 == 1000
mi_lp_problema += x11 >= 0
mi_lp_problema += x12 >= 0
mi_lp_problema += x13 >= 0
mi_lp_problema += x21 >= 0
mi_lp_problema += x22 >= 0
mi_lp_problema += x23 >= 0
mi_lp_problema.solve()

pulp.LpStatus[mi_lp_problema.status]
for variable in mi_lp_problema.variables():
    print ("{}={}".format(variable.name, variable.varValue))
```

    x11=1500.0
    x12=0.0
    x13=500.0
    x21=0.0
    x22=2000.0
    x23=500.0

```python

```

```python

```


# Simulate Euclid Images Using HST Ones

In this notebook, we are going to simulate step by a Euclid space telescope image using a HST one.

First things first, we start by preparing the worksapce.

```python
# to correctly show figures
%matplotlib inline

# import libraries here
import galsim
import numpy as np
import matplotlib.pyplot as plt
```

Here we are going to load the Euclid and HST required parameters.
> Euclid telescope specifications can be found [here](https://github.com/LSSTDESC/WeakLensingDeblending/blob/9f851f79f6f820f815528d11acabf64083b6e111/descwl/survey.py#L366).

```python
pixel_scale = 0.101
wcs = galsim.wcs.PixelScale(pixel_scale) #wcs: world coordinate system. Variable used to draw images in galsim
lam = 700  # nm
diam = 1.3    # meters
lam_over_diam = (lam * 1.e-9) / diam  # radians
lam_over_diam *= 206265  # Convert to arcsec
exp_time = 2260# exposure time
euclid_eff_area = 1.15 #effective area
```

Load the [COSMOS](https://github.com/GalSim-developers/GalSim/wiki/RealGalaxy%20Data) catalog and generate a galaxy and a PSF.

```python
catalog = galsim.COSMOSCatalog() # load catalog
img_len = 64 # arbitrary value, practical because power of 2
gal_ind = 133 # galaxy index in the catalog
gal = catalog.makeGalaxy(gal_ind, noise_pad_size=img_len * pixel_scale * np.sqrt(2))
psf = galsim.OpticalPSF(lam=lam, diam=diam, scale_unit=galsim.arcsec)
```

Now that we have loaded a galaxy from the catalog, let's rescale its flux such that it corresponds to a Euclid flux.

> Flux rescaling formula telescope specifications can be found [here](https://github.com/GalSim-developers/GalSim/blob/releases/2.2/examples/demo11.py#L110).

```python
hst_eff_area = 2.4**2 * (1.-0.33**2)
flux_scaling = (euclid_eff_area/hst_eff_area) * exp_time
gal *= flux_scaling
```

Apply the simulated Euclid PSF on the galaxy image.

```python
gal = galsim.Convolve(gal, psf)
```

Let's have a look at the galaxy and the PSF. In the galaxy image, noted $X$, we try to visually separate the noise (which standard deviation is noted $\sigma$) from the useful signal by applying the following transform:
\begin{equation}
\text{ArcSinh}\left(\frac{X}{k\sigma}\right).k\sigma
\end{equation}
> Technical precision: Usually the noise standard deviation is usually estimated with more accurate methods such as using a window to mask the galaxy then estimate the standard deviation on the rest of the samples which only contain noise. For sake of simplicity, in this example, we only considered an area of the image that only contains noise and estimated the noise standard deviation in it.

```python
# Get the standard deviation value of the noise for real images
gal_im = gal.drawImage(wcs=wcs, nx=img_len,ny=img_len)
# Empirically estimate the standard deviation by considering a part of the image containing only noise
hst_std = np.std(gal_im.array[0:25,0:25])

k=4

plt.figure(figsize=(20,20))
plt.subplot(121)
plt.title('ArcSinh of Convolved COSMOS Galaxy {}'.format(gal_ind))
plt.imshow(np.arcsinh(gal_im.array/(k*hst_std))*k*hst_std)
plt.subplot(122)
plt.imshow(np.log10(psf.drawImage(wcs=wcs, nx=img_len,ny=img_len).array))
plt.title(r'Log$_{10}$ Euclid-like PSF')
plt.show()
```

The noise that we see in image above corresponds to HST noise (which is also correlated due to the division by the HST PSF and the multiplication by the Euclid-like one), we are going to adapt this noise to Euclid. First we compute Euclid global noise standard deviation.

To do so, we compute $\lambda$ (in electrons per pixel), the Poisson parameter of the noise and aprroximate it with a white Gaussian noise such that its standard deviation is $\sqrt{\lambda}$.

> The $\lambda$ parameter corresponds to the `mean_sky_level` which expression can be find [here](https://github.com/LSSTDESC/WeakLensingDeblending/blob/9f851f79f6f820f815528d11acabf64083b6e111/descwl/survey.py#L110)

```python
def get_flux(ab_magnitude):
    zero_point = 6.85
    return exp_time*zero_point*10**(-0.4*(ab_magnitude-24))

sky_brightness = 22.9207
pixel_scale = 0.101
mean_sky_level = get_flux(sky_brightness)*pixel_scale**2 # it is the Poisson noise parameter

sigma = np.sqrt(mean_sky_level) # we modelize the noise as a Gaussian noise such that it std
                                # is the sqrt of the Poisson parameter
print('Euclid global noise standard deviation: {:.2f}'.format(sigma))
```

    Euclid global noise standard deviation: 20.66

Then we estimate the value of HST noise standard deviation and we take it into to account while adding the noise such that we end up with Euclid noise standard deviation.
> Reminder: For any independent random variables, the variance of the sum of those variables is equal to the sum of the variances.

```python
# Add noise
delta_std = np.sqrt(sigma**2 - hst_std**2)
random_seed = 24783923 #same as galsim demo 11
noise = galsim.GaussianNoise(galsim.BaseDeviate(random_seed), sigma=delta_std)
gal_im.addNoise(noise)
image = gal_im.array
```

Now that we simulated the Euclid observed image, let's show it and estimate its noise standard deviation as a check.

```python
plt.figure(2, figsize=(10,10))
plt.imshow(image)
plt.imshow(np.arcsinh(image/(k*sigma)*k*sigma))
plt.show()
print('Standard Deviation Value of Euclid Simulated Image: {:.2f}'.format(np.std(image[0:25,0:25])))
```


```python
import sympy
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
import seaborn as sns
from tqdm.notebook import tqdm
%matplotlib inline
```

We have 2 banners promoting a new sport club.
The first banner is aggressive: it focuses on the weight equipment we have and is very attractive to crossfitters, but completely can't convince runners. Another one makes the main focus on the cardio trainers we have and is much more attractive for runners. The neutral banner is also attractive to crossfitters, but it's not so cool as the first one.

Actually we don't know it. But that's what designers kept in mind when they create those banners.

Also let's imaging we don't know web site visitors interests. And ideally we show just one banner which is "the best" in general.

Let's define the world model. And use it further as a black box. Use bernoulli instead of binomial to make everything transparent.

```python
crossfitters_ratio = .48

aggressive = {"crossfitters": .68, "runners": .04}
neutral = {"crossfitters": .28, "runners": .4}

def test_banner(banner, shows):
    runners_dist = stats.bernoulli(banner["runners"])
    crossfitters_dist = stats.bernoulli(banner["crossfitters"])

    crossfitters_cnt = stats.bernoulli(crossfitters_ratio).rvs(shows).sum()
    runners_cnt = shows - crossfitters_cnt

    crossfitters_hits = crossfitters_dist.rvs(crossfitters_cnt).sum()
    runners_hits = runners_dist.rvs(runners_cnt).sum()

    return crossfitters_hits + runners_hits
```

To decide which banner is better we run experiment. We show both banners to random clients and make the conclusions out of the data we get.

Imagine we ran an experiment and computed the Convertion Rate as #Convertions / #Shows. That's a point estimation = 💩

To make some statistically significant conclusions we need to use confidence intervals or hypothesis checking methods.

Let's simplify everything a bit and sample from the true distribution directly. On practice we usually can't afford it and use bootstraping (delta-method/etc), but since we have the world model we don't need bootstrap.

```python
%%time
revenue_agressive = [test_banner(aggressive, 100) for _ in range(1000)]
revenue_neutral = [test_banner(neutral, 100) for _ in range(1000)]
sns.distplot(revenue_agressive, label="agressive")
sns.distplot(revenue_neutral, label="neutral")
plt.legend()
```

```python
%%time
revenue_agressive = [test_banner(aggressive, 1000) for _ in range(1000)]
revenue_neutral = [test_banner(neutral, 1000) for _ in range(1000)]
sns.distplot(revenue_agressive)
sns.distplot(revenue_neutral)
```

```python
%%time
revenue_agressive = [test_banner(aggressive, 100000) for _ in range(1000)]
revenue_neutral = [test_banner(neutral, 100000) for _ in range(1000)]

sns.distplot(revenue_agressive)
sns.distplot(revenue_neutral)
```

```python
%%time
revenue_agressive = [test_banner(aggressive, 1000000) for _ in range(1000)]
revenue_neutral = [test_banner(neutral, 1000000) for _ in range(1000)]

sns.distplot(revenue_agressive)
sns.distplot(revenue_neutral)
```

**Regret** - the money we lost on an experiment. If we have a magic oracle that tells you which banner is the best without any experiments you save ~ 347k - 343k = 4k €

You also may compute regret in counts of the "bad" banner shows. The loss in each show is the same. So, regret is 1M shows.

Due to we know how the real world behaves, let's check which banner is actually better.

```python
.48 * .68 + .52 * .04, .48 * .28 + .52 * .4
```

    (0.3472, 0.34240000000000004)

Usually you have too many factors and it's hard to say if two banners have real different conversion rate. The less difference the more audience you need to find the difference.

So, you want to have some early stopping method + the tool to compare more than 2 banners the same time.

There are some tools in classis statistics. But they look overcomplicated compared to the following approach. The other benefit of Multiarmed Bandits: you can use Contextual Multiarmed Bandits when you have additional information about users (gender, city, etc).

## Multiarmed bandit [Thompson sampling]

CTR doesn't exist, but we have CTR distribution.

* In fact there is a distribution of our knowledge about CTR.

Let's assume CTR is a Beta distribution. <s>Because of conjugate prior</s> Because I like Beta distribution.

We don't know any supported by data prior knowledge about the true CTR. Therefore it's better to use non-informative prior than use some particular value.

Don't use prejudices/preconception as a prior. Use either non-informative prior or something supported by data. Otherwise you would be loosing money when the model would fix your prior belives with the data.

How the Beta distribution looks like.

```python
xs = np.linspace(0, 1, 100)
plt.plot(xs, stats.beta(1, 1).pdf(xs), label="alpha = 1 beta = 1")
plt.plot(xs, stats.beta(.1, .1).pdf(xs), label="alpha = .1 beta = .1")
plt.plot(xs, stats.beta(7, 3).pdf(xs), label="alpha = 7 beta = 3")
plt.legend()
```

$Pr(A|B) = \frac{Pr(B|A)Pr(A)}{Pr(B)}$

$X$ - events (click/no-click)

CTR is a distribution, obviously it's defined on [0,1] (dom of Beta distribution).

$Pr(CTR|X) = \frac{Pr(X|CTR)Pr(CTR)}{Pr(X)} = \frac{Binomial(CTR) * Beta(\alpha, \beta)}{\int{Binomial(CTR) * Beta(\alpha, \beta)}} = \frac{Bernoulli(CTR) * Beta(\alpha, \beta)}{Const}$

Beta: $\frac{p^{\alpha - 1}(1 - p)^{\beta - 1}}{\mathrm {B}(\alpha, \beta)}$ Binomial: $\binom{N}{k} p^k(1 - p)^{N - k}$, **where p is a success probability, which is distributed as Beta**

$
Pr(CTR|X) = (p^{(\alpha + k) - 1} (1 - p)^{(\beta + N - k) - 1}) / Const
$

It has a shape of Beta distribution: $p^{\alpha - 1} (1 - p)^{\beta - 1}$

$\alpha_{new} = \alpha + k$

$\beta_{new} = \beta + N - k$

But we don't know the normalization constant. This curve may lay upper or lower than Beta distribution curve with same parameters. But we know the posterior is a some distribution and the square under the line should be = 1. So the only possible option - the Pr(CTR|X) curve is exactly the Beta distribution with $\alpha_{new}$ and $\beta_{new}$ parameters.

**$\alpha$ & $\beta$ correspond to the number of successes / failures**

The more data we've seen the more confident we're in the estimation.

```python
xs = np.linspace(0, 1, 100)
plt.plot(xs, stats.beta(7, 3).pdf(xs), label="alpha = 7 beta = 3")
plt.plot(xs, stats.beta(70, 30).pdf(xs), label="alpha = 21 beta = 9")
plt.plot(xs, stats.beta(700, 300).pdf(xs), label="alpha = 70 beta = 30")
plt.axvline(.7, 0, 1, color="red")
plt.legend()
```

### What banner to show

Let's create a lottery. On each show <s>sample</s> draw a dice out of Beta distribution and have a CTR point estimation.

```python
crossfitters_ratio = .48

aggressive = {"crossfitters": .68, "runners": .04}
neutral = {"crossfitters": .28, "runners": .4}

agressive_beta = {"alpha": 1, "beta": 1}
neutral_beta = {"alpha": 1, "beta": 1}

regret = 0
revenue = 0
for _ in tqdm(range(2000000)):
    aggresive_score = stats.beta(agressive_beta["alpha"], agressive_beta["beta"]).rvs()
    neutral_score = stats.beta(neutral_beta["alpha"], neutral_beta["beta"]).rvs()
    user_type = "crossfitters" if stats.bernoulli(crossfitters_ratio).rvs() > 0 else "runners"
    if aggresive_score > neutral_score:
        click = stats.bernoulli(aggressive[user_type]).rvs()
        if click:
            agressive_beta["alpha"] += 1
        else:
            agressive_beta["beta"] += 1
    else:
        regret += 1
        click = stats.bernoulli(neutral[user_type]).rvs()
        if click:
            neutral_beta["alpha"] += 1
        else:
            neutral_beta["beta"] += 1
    revenue += click
```

    HBox(children=(IntProgress(value=0, max=2000000), HTML(value='')))

```python
regret, revenue
```

    (33580, 693249)

```python
agressive_beta, neutral_beta
```

    ({'alpha': 681856, 'beta': 1284566}, {'alpha': 11395, 'beta': 22187})

```python
agressive_beta["alpha"] / (agressive_beta["alpha"] + agressive_beta["beta"]), neutral_beta["alpha"] / (neutral_beta["alpha"] + neutral_beta["beta"])
```

    (0.34674957867639805, 0.3393186826275981)

```python
crossfitters_ratio = .48

aggressive = {"crossfitters": .68, "runners": .04}
neutral = {"crossfitters": .28, "runners": .4}

agressive_beta = {"alpha": 1, "beta": 1}
neutral_beta = {"alpha": 1, "beta": 1}

regret = 0
revenue = 0
for _ in tqdm(range(200000)):
    aggresive_score = stats.beta(agressive_beta["alpha"], agressive_beta["beta"]).rvs()
    neutral_score = stats.beta(neutral_beta["alpha"], neutral_beta["beta"]).rvs()
    user_type = "crossfitters" if stats.bernoulli(crossfitters_ratio).rvs() > 0 else "runners"
    if aggresive_score > neutral_score:
        click = stats.bernoulli(aggressive[user_type]).rvs()
        if click:
            agressive_beta["alpha"] += 1
        else:
            agressive_beta["beta"] += 1
    else:
        regret += 1
        click = stats.bernoulli(neutral[user_type]).rvs()
        if click:
            neutral_beta["alpha"] += 1
        else:
            neutral_beta["beta"] += 1
    revenue += click
```

    HBox(children=(IntProgress(value=0, max=200000), HTML(value='')))

```python
xs = np.linspace(0.33, 0.36, 100)
plt.plot(xs, stats.beta(agressive_beta["alpha"], agressive_beta["beta"]).pdf(xs), label="agressive")
plt.plot(xs, stats.beta(neutral_beta["alpha"], neutral_beta["beta"]).pdf(xs), label="neutral")
plt.legend()
```

```python
regret, revenue
```

    (9896, 69634)

```python
agressive_beta, neutral_beta
```

    ({'alpha': 66283, 'beta': 123823}, {'alpha': 3353, 'beta': 6545})

```python
agressive_beta["alpha"] / (agressive_beta["alpha"] + agressive_beta["beta"]), neutral_beta["alpha"] / (neutral_beta["alpha"] + neutral_beta["beta"])
```

    (0.3486633772737315, 0.33875530410183874)

### So, why are we using Beta as a prior

We assume there is a true CTR and we have a distribution of our belief in CTR. Therefore this distribution should converge to delta-function. And have a domain of [0,1]. Beta satisfies these properties and is computational effecient.

```python

```


 SVD Y PCA

 INF-285 Computación Científica

 Autor: Francisco Andrades

Lenguaje: Python

Temas:

    - SVD
    - PCA
    - Image Compression

# Introducción

La compresión de Imágenes utilizando *SVD* se basa en que  la matriz $\Sigma$ representa los valores singulares de la matriz original, entonces se puede obtener una aproximación de la imagen original minimizando el rango de la matriz al eliminar los  valores singulares de menor valor, ya que estos representan una "menor información" de la imagen. De este forma, por ejemplo si $\Sigma$ es de tamaño $n\times n$, se pueden omitir los $\sigma$ menos significativos obteniendo $\tilde{\Sigma}$ de tamaño $m\times m$, $m<n$.

Por otro lado, también se puede utilizar el análisis de componentes principales (PCA) para la compresión de imágenes al reducir la dimensión de la matriz de la imagen y proyectar esas nuevas dimensiones en una nueva imagen reteniendo la información importante de la imagen original

En esta tarea se busca comprimir un archivo *GIF*, el cual consiste de una secuencia de multiples imagenes, utilizando *SVD* y *PCA* para poder comparar ambos métodos y analizar la relación entre ambos.

```python
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image, ImageSequence
import matplotlib.image as mpimg
```

```python
def plotAnimation(animation):
    """
    Parameters
    ----------
    animimation         : (frames, rows, cols) array
                          GIF array

    Returns
    -------
    Animation plots      : None
    """
    i = 0
    for frame in animation:
        plt.imshow(frame, cmap=plt.cm.gray)
        plt.axis('off')
        plt.show()
        i = i+1
```

```python
def gifToArray(gif_file):
    """
    Parameters
    ----------
    gif_file             : string
                          GIF path

    Returns
    -------
    data                 : (frames, rows, cols) array
                          NumPy array with GIF pixel values
    """
    im = Image.open(gif_file)
    data = list()
    for frame in ImageSequence.Iterator(im):
        tmp = np.array(im.convert('L'))
        data.append(tmp)
    data = np.array(data)
    return data
```

Podemos considerar un *GIF* como una colección de $p$ *frames*, donde un *frame* es una martriz $F\in\mathbb{R}^{r\times c}$ con $r$ el número de filas y $c$ en número de columnas de esta imagen. Ahora, si $(f_k)_{i,j}$ corresponde al elemento $i,j$ del $k$-ésimo *frame*, vamos a definir $\mathbf{f}_{i,j}=\langle (f_1)_{i,j}, (f_2)_{i,j},\dots,(f_p)_{i,j}\rangle$,
es decir, este vector corresponde a los valores de los $p$ frames de la coordenada $(i,j)$ del *GIF*.

Finalmente, para trabajar con los algoritmos, vamos a construir la matriz $G \in \mathbb{R}^{q\times p}$, donde $q=r\times c$ de cada *frame*, y que se define como:

\begin{equation}
    G =
    \left[
    \begin{array}{c}
        \mathbf{f}_{1,1} \\ \hline
        \mathbf{f}_{1,2} \\ \hline
        \dots \\ \hline
        \mathbf{f}_{r,c}
    \end{array}
    \right]
\end{equation}

----
## Funciones a Implementar

1. Crear la función ```createG(data)``` que recibe ```data``` el arreglo ```NumPy``` con la información del GIF, y retorna el arreglo $G$ definido anteriormente. (10 puntos)

```python
def createG(data):
    """
    Parameters
    ----------
    data             : (frames, rows, cols) array
                       NumPy array with GIF pixel values

    G                : (q, p) array
                       G matrix
    """
    G = list()
    for frame in data:
        fila_nueva = []
        for row in frame:
            for i in row:
                fila_nueva.append(i)
        G.append(np.array(fila_nueva))
    G = np.array(G)
    return G.transpose()
```

2. Crear la función ```restoreGIF(data)``` que recibe los datos procesados ```data``` y ```shape``` que contiene la tupla ```(frames, rows, cols)```, la dimensión original del *GIF*. Esta función retorna la reconstrucción del GIF. (10 puntos)

```python
def restoreGIF(data, shape):
    """
    Parameters
    ----------
    data             : (q, p) array
                       G matrix
    shape            : tuple (frames, rows, cols)
    Returns
    -------
    reshaped_data    : (frames, rows, cols) array
                       NumPy array with GIF pixel values

    """

    n_frames = shape[0]
    n_rows = shape[1]
    n_col = shape[2]
    reshaped_data = []
    G = data.transpose()

    for frame in G:
        frame_original = []
        for cont in range(n_rows):
            row_original = frame[cont*n_col:(cont+1)*n_col]
            frame_original.append(row_original)

        reshaped_data.append(frame_original)

    reshaped_data = np.array(reshaped_data)
    return reshaped_data
```

### SVD
3. Implementar la función ```G_SVD(G, m)``` que reciba la matriz $G$ y los $m$ componentes que se utilizarán para comprimir el *GIF* utilizando *SVD*. La función debe retornar $U$, $\textrm{diag}(\Sigma)$ y $V^T$. Además, implementar la función ```SVD_G(U, s, Vt)``` que recibe las matrices generadas por el *SVD* y retorne la reconstrucción de la matriz $G$. (30 puntos)

```python
# G to SVD
def G_SVD(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    U             : (q, m)-array
                    SVD U matrix
    s             : m-array
                    Singular values
    Vt            : (m, p)-array
                    SVD V^T matrix
    """
    # Apply SVD
    u, n, vt =np.linalg.svd(G,full_matrices = False)

    #tomar las primeras m columnas de u

    U = []
    for row in u:
        fila_nueva = row[:m]
        U.append(fila_nueva)

    #tomar las primeras m filas y columnas de n

    s = n[:m]
    #tomar las primeras m filas de vt
    Vt = vt[:m]

    U = np.array(U)
    s = np.array(s)
    Vt = np.array(Vt)
    return U, s, Vt

# SVD to 'compressed' G
def SVD_G(U, s, Vt):
    """
    Parameters
    ----------
    U             : (q, m)-array
                    SVD U matrix
    s             : m-array
                    Singular values
    Vt            : (m, q)-array
                    SVD V^T matrix
    Returns
    -------
    B             : (p, q)-array
                    "Compressed" G
    """

    b1 = []
    cont = 0
    for row in Vt:
        b1.append(row*s[cont])
        cont = cont+1

    B = U.dot(b1)
    B = np.array(B)
    return B
```

### PCA
4. Implementar la función ```G_PCA(G, m)``` que reciba la matriz $G$ y los $m$ componentes que se utilizarán para comprimir el *GIF* utilizando *PCA*. La función debe retornar $PC$, $Y$ y $\mu$. Además, implementar la función ```PCA_G(PC, Y, mu)``` que recibe las matrices generadas por *PCA* y retorne la reconstrucción de la matriz $G$. Para esto debe utilizar la funcion de SVD implementada en el punto anterior. (35 puntos)

```python
def G_PCA(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    PC             : (p, m)-array
                     first m principal components
    Y             : (q,m)-array
                    PC Scores
    mu           : (p)-array
                    Average per column
    """

    mu = G.mean(0)

    Z = G - mu

    U,s,Vt = G_SVD(Z,m)

    v = Vt.transpose()

    PC = []
    for row in v:
        fila_nueva = row[:m]
        PC.append(fila_nueva)

    PC = np.array(PC)

    aux = np.zeros((m, m))
    np.fill_diagonal(aux, s)

    Y = U.dot(aux)

    return  PC, Y, mu
```

```python
def PCA_G(PC, Y, mu):
    """
    Parameters
    ----------
    PC             : (p, m)-array
                     first m principal components
    Y             : (q,m)-array
                    PC Scores
    mu           : (p)-array
                    Average per column
    Returns
    -------
    B            : (q, p)-array
                    "Compressed" G
    """

    Vt = PC.transpose()

    matriz = Y.dot(Vt)

    B = matriz + mu
    return B
```

## Preguntas

Para responder las siguientes preguntas, debe implementar las funciones propuestas

#### 1. ¿Cuál sería el costo de almacenamiento en MB usando $m$ vectores singulares? (5 puntos)

```python
def SVD_size(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    size          : Float
                    total size of SVD return
    """
    U,s,Vt = G_SVD(G,m)
    size1 = U.nbytes
    size2 = Vt.nbytes
    size3 = s.nbytes
    size = size1 + size2 + size3
    size = size/1024
    return size/1024

def PCA_size(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    size          : Float
                    total size of PCA return

    """
    PC , Y , mu = G_PCA(G, m)
    size1 = PC.nbytes
    size2 = Y.nbytes
    size3 = mu.nbytes
    size = size1 + size2 + size3
    size = size/1024
    return size/1024
```

#### 2. ¿Cuál sería el *gif* resultante con $m$ componentes? (5 puntos)

```python
def print_animation_SVD(G, m,shape):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    La funcion no debe retornar nada, solo mostrar las imagenes de los frames reconstruidos
    """
    U,s,Vt = G_SVD(G,m)

    B = SVD_G(U,s,Vt)
    restore1 = restoreGIF(B,shape)
    plotAnimation(restore1)
    return

def print_animation_PCA(G, m,shape):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    La funcion no debe retornar nada, solo mostrar las imagenes de los frames reconstruidos
    """

    PC , Y , mu = G_PCA(G, m)
    B = PCA_G(PC, Y, mu)
    restore1 = restoreGIF(B,shape)
    plotAnimation(restore1)
    return
```

#### 3. ¿Cual sería el error en función de $m$? (Calcule el error utilizando la norma-2) (5 puntos)

Considere calcular el error de la siguiente manera: $||G-B_m||_2$, donde $G$ corresponde a la matriz definida anteriormente y $B_m$ a la matriz "comprimida" utilizando los métodos correspondientes para un $m$ particular.

```python
def compression_error_SVD(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    error          : Float
                    total size of PCA return
    """

    U,s,Vt = G_SVD(G,m)

    B = SVD_G(U,s,Vt)

    H = G-B
    error = np.linalg.norm(H,2)
    return error

def compression_error_PCA(G, m):
    """
    Parameters
    ----------
    G             : (q, p)-array
                    G matrix
    m             : int
                    Number of components
    Returns
    -------
    error         : Float
                    total size of PCA return
    """
    PC , Y , mu = G_PCA(G, m)
    B = PCA_G(PC, Y, mu)
    H = G-B
    error = np.linalg.norm(H,2)
    return error
```

# Prueba

Para verificar sus algoritmos, pruebe las funciones desarrolladas para $m=10$.

```python
data = gifToArray("somebody.gif")
shape =data.shape
G = createG(data)
print("Size SVD:",SVD_size(G,10))
print("Size PCA:",PCA_size(G,10))
#print_animation_SVD(G, 10,shape) #descomentar para imprimir los frames
#print_animation_PCA(G, 10,shape) #descomentar para imprimir los frames
print("Error SVD:",compression_error_SVD(G, 10))
print("Error PCA:",compression_error_PCA(G, 10))
```

    Size SVD: 6.3668060302734375
    Size PCA: 6.368255615234375
    Error SVD: 10888.513291792182
    Error PCA: 10872.976738591024

```python

```


# Demo 1: Flow

## Background/Motivation

Flow is a fundamental driver of processes in the surface and subsurface. In this set of demonstrations we will introduce

  * Richards Model for variably saturated subsurface flow
  * Diffusion Wave model for overland flow (surface flow)
  * A fully coupled (integrated hydrology) model

For each model we will provide the mathematical equations, highlight information about the discretization and provide tips on setting boundary conditions, initial conditions, source terms and key parameters.

## Subsurface Flow (Richards Equation)

We can provide the essentials of the mathematical models using LaTeX.  For example, Richards (1931) model for variably saturated flow:

$\newcommand{porosity}{\phi}$
$\newcommand{dens}{n_l}$
$\newcommand{sat}{s}$
$\newcommand{flux}{\mathbf{q}}$
$\newcommand{krel}{k_r}$
$\newcommand{kabs}{\kappa}$
$\newcommand{viscosity}{\mu}$

$$
\begin{align}
  \frac{\partial}{\partial t} (\porosity \dens \sat) &+ \nabla \cdot \flux = 0 \\
  \flux &= - \dens \frac{\krel \kabs}{\viscosity} (\nabla p + \rho \mathbf{g})
\end{align}
$$

**Note that this uses latex macros defined in this cell.**

Blah, Blah, lots more about the model and equations ...

### Problem Specification

We consider three cases of the steady-state flux in a layered soil column 1[m] x 1 [m] x 2[m],

The presure profiles should match that in the Vanderborght paper. The difference between the cases is as follows:

  * case 1: is 0.5 m of clay and 1.5 m of sand;
  * case 2: is 0.5 m of loam and 1.5 m of sand;
  * case 3: is 1.5 m of loam and 0.5 m of sand.

```python
matplotlib inline
```

```python
import sys,os
sys.path.append(os.path.join(os.environ['ATS_SRC_DIR'],'tools', 'utils'))
import ats_xdmf
import plot_column_data
import numpy as np
from matplotlib import pyplot as plt
```

### ATS Input File Notes

I'm not sure the best way to step through and/or discuss the input file.  But we can highlight some key elements in a markdown cell.  For example, it would be really good to clarify boundary and initial conditions as these seem to give users significant problems.

**Initial Condition**

We set the initial water table at -3[m] below the surface,

```xml
<ParameterList name="initial condition" type="ParameterList">
   <Parameter name="hydrostatic head [m]" type="double" value="-3" />
   <Parameter name="hydrostatic water density [kg m^-3]" type="double" value="1000" />
</ParameterList>
```
Where we note there is a dependence on a mesh parameter as well,

```xml
<Parameter name="build columns from set" type="string" value="surface" />
```
**Boundary Conditions**

Etc. Etc.

### Running ATS

Things run a bit slowly under Jupyter in Docker (this simple run takes 10-15 minutes).  So it may be best to store results in the repo.  Some of it is related to disk performance, so minizing I/O and the screen output as well can help.  If there's interest we can dig in a bit more on these issues.

```python
# Note we can't get subprocess.PIPE to work within Docker, but os.system works fine.
os.system("mkdir richards-infiltration.demo")
os.system("(cd richards-infiltration.demo; ats --xml_file=../infiltration.xml 2>&1 > sim.stdout)")
```

    ASCEMIO_Info: Preferred group size is set to nprocs/numIOgroups (1/1)
    ASCEMIO_Info: Preferred group size is set to 1
    ASCEMIO_Info: Preferred group size is set to nprocs/numIOgroups (1/1)
    ASCEMIO_Info: Preferred group size is set to 1

    0

### Visualize the Results

We should be clear about what the simulation shows physically about the system, and have some visualization that supports this narrative.

Defining some functions in python seems ok, but probably want to minimize in favor of using generic capabilities in the ATS tools.

```python
def plot(dirname, title):
    """A generic plotting function that plots saturation as a function of depth."""
    fig = plt.figure()

    vis = ats_xdmf.VisFile(dirname, time_unit='d')
    vis.loadMesh(columnar=True)

    # plot the solution
    ax = fig.subplots(1,1)
    plot_column_data.plot_subsurface(vis, ['saturation_liquid',], ax, None, cmap='jet')

    ax.set_xlabel('saturation [-]')
    ax.set_ylabel('z [m]')

```

```python
plot("./richards-infiltration.demo", "infiltration example")
```

## Overland Flow (Diffusion Wave Equation)

## Integrated Hydrology

```python

```


# Chapter 1 - Water Balance
Execute the following code block to initialize this notebook

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

The water balance is an accounting of the movement and storage of water within the biosphere. A generalized water balance equation is defined as,
\begin{equation}
P+w = I+R_s+R_g+E_{surface}+E_{plant}+E_{soil}+\Delta V_{soil}/t+\Delta SWE/t
\end{equation}

$\frac{\partial S}{\partial t}=[P(t)-L\times L\times E_t(t)-E_s(t)-E_{vs}(t)-E_{ss}(t)-R_s(t)-R_g(t)-v_s(t)+w(t)]\\
=\frac{\partial}{\partial t}[V_{vs}(t)+V_{ss}(t)+V_{gs}(t)+V_{SWE}(t)]$

## Precipitation

```python
A=4
A+3
```

    7

## Infiltration

The change in storage can be evaluated as:
\begin{equation}
\Delta S = I-O
\end{equation}

# Precipitation Intensity-Duration-Frequency (IDF) Curves

```python
Prec=pd.read_csv("./Data/Annual_Freq_Data.txt", delim_whitespace=True, index_col=0)
N=len(Prec)
print('Number of years of annual data: ',N)
```

    Number of years of annual data:  21

Take a look at the first five rows of data, which shows total event precipitation in (mm) for each year, for ecah duration period.

```python
Prec.head()
```

      5m
      10m
      15m
      30m
      1h
      2h
      6h
      12h
      24h

      Year

      1985
      2.8
      5.3
      8.1
      10.9
      13.7
      14.4
      24.2
      28.0
      30.4

      1986
      2.5
      3.9
      4.4
      5.9
      8.6
      14.6
      36.8
      56.3
      84.7

      1987
      1.5
      2.5
      3.2
      5.5
      9.9
      17.7
      33.8
      43.2
      65.3

      1988
      2.0
      3.2
      4.2
      5.3
      6.8
      11.1
      27.7
      45.0
      51.8

      1989
      3.0
      4.3
      5.2
      6.9
      9.3
      15.2
      30.0
      45.6
      50.9

Looking at the mean and standard deviation of all available years:

```python
pd.DataFrame([Prec.mean() , Prec.std()] , index=['Mean','Std'])
```

      5m
      10m
      15m
      30m
      1h
      2h
      6h
      12h
      24h

      Mean
      2.652381
      3.842857
      4.857143
      7.061905
      10.223810
      15.42381
      31.757143
      44.728571
      60.690476

      Std
      0.819524
      1.280458
      1.797101
      2.498295
      2.867561
      2.61092
      6.036685
      8.791083
      16.689335

$p=\frac{1}{T}=\frac{m}{N+1}$

```python
F30m = pd.DataFrame(Prec['30m'].sort_values(ascending=False)) # sorted (descending) values for 30m duration events
F30m['m'] = np.arange(1 , N+1) # get index value
F30m['p'] = m/(N+1)
F30m['T'] = 1/p
F30m
```

      30m
      m
      p
      T

      Year

      1996
      15.9
      1
      0.045455
      22.000000

      1985
      10.9
      2
      0.090909
      11.000000

      2004
      9.2
      3
      0.136364
      7.333333

      2005
      8.5
      4
      0.181818
      5.500000

      2003
      8.1
      5
      0.227273
      4.400000

      1993
      7.4
      6
      0.272727
      3.666667

      1989
      6.9
      7
      0.318182
      3.142857

      1997
      6.7
      8
      0.363636
      2.750000

      1991
      6.4
      9
      0.409091
      2.444444

      1990
      6.2
      10
      0.454545
      2.200000

      1999
      6.0
      11
      0.500000
      2.000000

      1986
      5.9
      12
      0.545455
      1.833333

      1995
      5.9
      13
      0.590909
      1.692308

      2000
      5.9
      14
      0.636364
      1.571429

      1994
      5.8
      15
      0.681818
      1.466667

      2002
      5.7
      16
      0.727273
      1.375000

      1987
      5.5
      17
      0.772727
      1.294118

      2001
      5.5
      18
      0.818182
      1.222222

      1992
      5.3
      19
      0.863636
      1.157895

      1988
      5.3
      20
      0.909091
      1.100000

      1998
      5.3
      21
      0.954545
      1.047619

```python
F30m.plot('T' , '30m' , kind='scatter' , logx=True)
plt.grid()
plt.xlim(1,100)
```

Gumbel Type-1 Distribution
$$G(x,\mu,\beta) = \frac{1}{\beta}e^{\frac{x-\mu}{\beta}e^{-e^{\frac{x-\mu}{\beta}}}}$$

$$K_t=-\frac{\sqrt{6}}{\pi}[0.5772+ln(ln\frac{T}{T-1})]$$

```python
Kt=-6**.5/np.pi*(0.5772+np.log(np.log(T/(T-1))))
```

```python
Xbar=Prec['30m'].mean()
S=Prec['30m'].std()
Xt=Xbar+Kt*S
```

```python
T_calc=np.array([2,5,10,25,50,100,1000])
Kt_calc=-6**.5/np.pi*(0.5772+np.log(np.log(T_calc/(T_calc-1))))
```

```python
(Xbar+Kt_calc*S)/.5
```

    array([13.30300959, 17.71864276, 20.64217613, 24.3360683 , 27.07640852,
           29.79651451, 38.78459445])

```python
Intensity=(np.matrix(Prec.mean()).T+np.dot(np.matrix(Prec.std()).T , np.matrix(Kt_calc)))/\
np.matrix([5/60,10/60,15/60, 30/60, 1, 2, 6, 12, 24]).T
Intensity=pd.DataFrame(Intensity,columns=T_calc,index=[5/60,10/60,15/60, 30/60, 1, 2, 6, 12, 24])
Intensity
```

      2
      5
      10
      25
      50
      100
      1000

      0.083333
      30.213074
      38.903920
      44.658017
      51.928333
      57.321869
      62.675580
      80.365913

      0.166667
      21.795083
      28.584551
      33.079769
      38.759490
      42.973031
      47.155461
      60.975513

      0.250000
      18.247718
      24.600322
      28.806298
      34.120560
      38.062984
      41.976297
      54.907107

      0.500000
      13.303010
      17.718643
      20.642176
      24.336068
      27.076409
      29.796515
      38.784594

      1.000000
      9.752749
      12.286897
      13.964724
      16.084662
      17.657353
      19.218432
      24.376724

      2.000000
      7.497454
      8.651128
      9.414960
      10.380064
      11.096034
      11.806716
      14.155034

      6.000000
      5.127581
      6.016713
      6.605397
      7.349200
      7.900995
      8.448716
      10.258558

      12.000000
      3.607037
      4.254448
      4.683091
      5.224683
      5.626466
      6.025283
      7.343098

      24.000000
      2.414537
      3.029073
      3.435949
      3.950038
      4.331418
      4.709983
      5.960879

```python
Intensity.plot()
plt.xlabel('Storm Duration (hrs)')
plt.ylabel('Intensity (mm/hr)')
plt.grid()
```

```python

```


# CHEM 1000 - Spring 2022
Prof. Geoffrey Hutchison, University of Pittsburgh

## 10 Differential Equations - First Order - Examples

Chapter 10 in [*Mathematical Methods for Chemists*](http://sites.bu.edu/straub/mathematical-methods-for-molecular-science/)

By the end of this session, you should be able to:
- Understand basic naming / categories of differential equations
- Solve typical first-order ordinary differential equations
- Understand the general solution to first-order linear equations
   - See some examples from chemical kinetics

### Differential Equations - Categories / Naming

First off, a [**differential equation**](https://en.wikipedia.org/wiki/Differential_equation) is simply some type of mathematical equation that includes some type of derivative of a function.

Wikipedia:
>In applications, the functions generally represent physical quantities, the derivatives represent their rates of change, and the differential equation defines a relationship between the two. Such relations are common; therefore, differential equations play a prominent role in many disciplines including engineering, physics, chemistry, economics, and biology.

Solving differential equations generally depends on the *type* of "diff eq", so there are some general categories:

An **ordinary differential equation** (ODE) only involves one variable and thus ordinary derivatives.

A **partial differential equation** (PDE) includes multiple variables and partial derivatives.

We also categorize differential equations by their **order** defined by the term with the highest derivative:
- First derivatives = first-order differential equation
- Second derivatives = second-order differential equation
- etc.

(Most differential equations in chemistry are either first-order or second-order.)

#### Summary:

In short, the main ways to categorize differential equations. We ask because the types influence how we solve them.

- Order (first order, second order, etc.)
- Ordinary vs. Partial
- Linear vs. Nonlinear
- Homogenous vs. Non-homogenous

#### Examples

Homogeneous second-order linear ordinary differential equation

$$
\frac{d^{2} u}{d x^{2}}-x \frac{d u}{d x}+u=0
$$

Heterogeneous first-order nonlinear ordinary differential equation:

$$
\frac{d u}{d x}=u^{2}+4
$$

So what is this equation?

$$
\frac{d^{2} u}{d x^{2}}+\omega^{2} u=0
$$

### General Solutions (Homogeneous)

Let's take the general **homogeneous** first-order equation:

$$
\frac{dy}{d x} +q(x) y=0
$$

This is also known as a "reduced" equation because there is no constant term.

How do we solve this?

We can try "separation of variables." That is, if we get all the terms in $x$ on one side and $y$ on the other:

$$
\frac{dy}{dx} = -q(x) y
$$

Then...

$$
\frac{1}{y} d y=-q(x) d x
$$

Now we can integrate both sides:

$$
\int \frac{1}{y} d y=-\int q(x) d x
$$

The left side is really easy to integrate:

$$
\ln y(x)=-\int q(x) d x+\mathcal{C}
$$

Taking the exponent of both sides:

$$
y(x)=A \exp \left[-\int q(x) d x\right]
$$

Of course if $q(x)$ is a constant, this is easy, e.g.:

$$
y(x)=A e^{-\alpha x}
$$

Notice this is the same as our original example with kinetics of a chemical reaction.

### General Solutions (Non-Homogeneous)

What about the **non-homogeneous** case?

$$
\frac{d}{d x} y(x)+q(x) y(x)=r(x)
$$

Unfortunately, we can't use separation of variables like we did in the homogeneous case above.

But let's assume the form is fairly similar, maybe an *ansatz* like this:

$$
y(x)=u(x) \exp \left[-\int q(x) d x\right]
$$

In other words, we're still using a similar exponential form, but the *ansatz* guesses that we'll need more than just a constant in front of the exponential function, so $u(x)$ is needed because of the $r(x)$ form in the equation.

Let's first take the left side of our equation:

$$
\frac{dy}{dx} + q(x) y(x) = \frac{d}{d x} u(x) \times \exp \left[-\int q(x) d x\right] -u(x) \times q(x) \exp \left[-\int q(x) d x\right] + q(x) u(x) \exp \left[-\int q(x) d x\right]
$$

That simplifies nicely!

$$
\frac{d}{d x} u(x) \times \exp \left[-\int q(x) d x\right]=r(x)
$$

Okay, let's clean this up a bit...

$$
\frac{d u(x)}{dx \times \exp \left[\int q(x) d x\right]} = r(x)
$$

We can then use separation of variables...

$$
d u(x)=r(x) \exp \left[\int q(x) d x\right] d x
$$

Integrate both sides and we get this somewhat messy:

$$
u(x)=\int r(x) e^{ \left[\int q(x) d x\right]} d x+\mathcal{C}
$$

Honestly, that's not too weird:
1. Take the integral of $q(x) dx$
2. Take the integral of $r(x) \times e^{integral} dx$

Remember that we need *y(x)* not *u(x)*:

$$
y(x)=\left[\int r(x) e^{\int q(x) d x} d x+\mathcal{C}\right] \times e^{-\int q(x) d x}
$$

The formula is a bit complicated, but once you integrate $q(x)$ and $r(x)$ it simplifies quickly to a general solution.

### Examples

The main take-home message is that with some integration, we can solve a wide variety of linear first-order differential equations - both homogeneous and non-homogeneous.

#### Equilibrium Kinetics

Okay, let's consider a simple chemical equilibrium:

$$
\mathrm{A} \underset{k_{b}}{\stackrel{k_{f}}{\rightleftharpoons}} \mathrm{B}
$$

We can then write the change in concentration of $A$:
$$
\frac{d A}{d t}=-k_{f} A+k_{b} B
$$

Since at the start, there is no $B$ we can write it as a function of $A$:

$$
\frac{d A}{d t}=-k_{f} A+k_{b}\left(A_{0}-A(t)\right)=-\left(k_{f}+k_{b}\right) A(t)+k_{b} A_{0}
$$

We can clean this up to look like our general non-homogeneous first-order equation above:

$$
\frac{d A}{d t}+\left(k_{f}+k_{b}\right) A(t)=k_{b} A_{0}
$$

**Good news!** We can solve this.

First off, we need the integral of our $q(t)$

$$
\int\left(k_{f}+k_{b}\right) d t=\left(k_{f}+k_{b}\right) t
$$

Now we have a solution:

$$
A(t)=e^{-\left(k_{f}+k_{b}\right) t}\left[\int k_{b} A_{0} e^{\left(k_{f}+k_{b}\right) t} d t+\mathcal{C}\right]
$$

Let's take the other integral;

$$
A(t) = \left[A_{0} \frac{k_{b}}{k_{f}+k_{b}}+\mathcal{C} e^{-\left(k_{f}+k_{b}\right) t}\right]
$$

At the start (t = 0):

$$
A(0)=\left[A_{0} \frac{k_{b}}{k_{f}+k_{b}}+\mathcal{C}\right]=A_{0}
$$

So our ***final*** equation is:

$$
A(t)=A_{0} \frac{k_{b}}{k_{f}+k_{b}}\left(1+\frac{k_{f}}{k_{b}} e^{-\left(k_{f}+k_{b}\right) t}\right)
$$

```python
# Let's plot it!
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
plt.style.use('./chem1000.mplstyle')
```

```python
kf = 0.1 # per second
kb = 0.3 # per second
A0 = 1 # molar

t = np.arange(0, 20, 0.1)

A = A0 * (kb / (kf+kb))*(1 + (kf/kb)*np.exp(-(kf+kb)*t))
plt.plot(t, A)

B = A0 - A
plt.plot(t, B)

plt.xlabel("Time (s)")
plt.xlim(0, 20)
plt.ylabel("Concentration (M)")
plt.ylim(0, 1)
plt.show()
```

Note that the equilibrium concentrations would be:

$$
A_{e q}=A_{0} \frac{k_{b}}{k_{f}+k_{b}} \quad B_{e q}=A_{0} \frac{k_{f}}{k_{f}+k_{b}}
$$

### Another Example

Solve the first-order differential equation (q. 10.2):

$$
\frac{d y(x)}{d x}=x^{2}-3 x^{2} y(x)
$$

This looks a little different, so let's re-organize:

$$
\frac{d y(x)}{d x} + 3 x^{2} y(x) =x^{2}
$$

So our $q(x)$ is $3x^2$:

$$
\int 3x^2 dx = x^3 + C
$$

Now we need to solve:

$$
\int r(x) e^{x^3 + C} dx = \int x^2 e^{x^3 + C} dx
$$

So we'll need to do some integration by parts...

```python
# Let's do some examples
from sympy import init_session
init_session()
```

```python
C = symbols('C')
# get integral of q(x)
q = 3*x**2
int_q = integrate(q, x)
```

```python
r = x**2
u = integrate(r * exp(int_q), x)
u
```

So the full equation will be:

$$
y = e^{-x^3+C} \times 1/3 e^{x^3 + C}
$$

```python
# let's check this
y = exp(-x**3 + C)*integrate(r * exp(x**3 + C), x)
y
```

-------
This notebook is from Prof. Geoffrey Hutchison, University of Pittsburgh
https://github.com/ghutchis/chem1000


Határozzuk meg az alábbi ábrán látható tartó súlypontvonalának eltolódását leíró $v\left(x\right)$ függvényt végeselemes módszer használatával, síkbeli egyenes gerendalemek alkalmazásával.
Vizsgáljuk meg a végeselemes megoldással kapott hajlítónyomatéki igénybevétel hibáját az egyes szakaszokon.
Határozzuk meg az $x = a/2$ keresztmetszetben a hajlítónyomatéki igénybevétel nagyságát 2, illetve 3 síkbeli egyenes gerendaelem alkalmazásával.
A tartók két különböző átmérőjű ($d_1 = 2d$, illetve $d_2 = d$) kör keresztmetszetű tartókból
vannak összeépítve.
A tartók anyaga lineárisan rugalmas, homogén, izotrop. A $d_1$ átmérőjű rész rugalmassági modulusza $E$, míg a $d_2$ átmérővel rendelkező részé $4E$.

```python
import sympy as sp
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

sp.init_printing()
```

```python
p, xi, L, I, E = sp.symbols("p, xi L I E")
```

```python
Nv = 1/8*sp.Matrix([2*(1-xi)**2*(2+xi),
                 L*(1-xi)**2*(1+xi),
                 2*(1+xi)**2*(2-xi),
                 L*(1+xi)**2*(xi-1)])
Nv
```

```python
Nfi=sp.diff(Nv,xi)*(2/L)
sp.simplify((1/L)**(-1)*Nfi)
```

```python
Bv=sp.diff(Nv,xi,2)*(2/L)**2
sp.simplify((1/L**2)**(-1)*Bv)
```

```python
sp.diff(Bv,xi)*2/L
```

```python
KeBEAM1D = I*E*sp.integrate(Bv*(Bv.T),(xi,-1,1))*L/2
(I*E/L**3)**(-1)*KeBEAM1D
```

```python
Fp = p*sp.integrate(Nv,(xi,-1,1))*L/2
(L*p/12)**(-1)*Fp
```

```python
def Bvfgv(xi,L):
    return np.multiply(1/L**2,np.array([6*xi, L*(3*xi-1),-6*xi,L*(3*xi+1)]))
```

```python
def Nvfgv(xi,L):
    return np.array([1/4*(1-xi)**2*(2+xi),
                 L/8*(1-xi)**2*(1+xi),
                 1/4*(1+xi)**2*(2-xi),
                 L/8*(1+xi)**2*(xi-1)])
```

```python
def dddNvfgv(xi,L):
    return np.multiply(1/L**3,np.array([12, 6*L, -12, 6*L]))
```

```python
def Nfifgv(xi,L):
    return np.multiply(1/L,np.array([1.5*xi**2-1.5,
                     L*(0.75*xi+0.25)*(xi-1),
                     -1.5*xi**2+1.5,
                     L*(0.75*xi-0.25)*(xi+1)]))
```

```python
def KeBEAM1D(I,E,L):
    return np.multiply(I*E/L**3, np.array([[12,   6*L,    -12,  6*L],
                                           [6*L,  4*L**2, -6*L, 2*L**2],
                                           [-12,  -6*L,    12,  -6*L],
                                           [6*L, 2*L**2, -6*L, 4*L**2]]))
```

```python
def Fp(p,L):
    return np.multiply(L*p/12,np.array([6,L,6,-L]))
```

```python
Ne1 = 2
Ne2 = 1
a=800e-3
b=400e-3
d=20e-3
E=50e9
nu=0.3
F1=2500
M1=-500
p0=-5000
```

```python
I1 = (2*d)**4*np.pi/64
I2 = (d)**4*np.pi/64
E1 = E
E2 = 4*E
Ke1 = KeBEAM1D(I1,E1,a/Ne1)
Ke2 = KeBEAM1D(I2,E2,b/Ne2)
```

```python
elemSZF1 = [np.arange(2*i,2*(i+2)) for i in np.arange(Ne1)]
elemSZF1
```

    [array([0, 1, 2, 3]), array([2, 3, 4, 5])]

```python
elemSZF2 = [np.arange(2*i,2*(i+2)) for i in np.arange(Ne1,Ne1+Ne2)]
elemSZF2
```

    [array([4, 5, 6, 7])]

```python
KG=np.zeros((2*(Ne1+Ne2+1),2*(Ne1+Ne2+1)))

for eind in range(Ne1):
    KG[np.ix_(elemSZF1[eind],elemSZF1[eind])]+=Ke1

for eind in range(Ne2):
    KG[np.ix_(elemSZF2[eind],elemSZF2[eind])]+=Ke2
```

```python
FG=np.zeros(2*(Ne1+Ne2+1))
for eind in range(Ne1):
    FG[elemSZF1[eind]] += Fp(p0,a/Ne1)
FG[elemSZF1[-1][2]] += F1
FG[elemSZF2[-1][3]] += M1
```

```python
fixSZF=np.array([0,1,elemSZF2[-1][2]])
```

```python
szabadSZF=[i for i in range(0,2*(Ne1+Ne2+1)) if i not in fixSZF]
szabadSZF
```

```python
KGkond = KG[np.ix_(szabadSZF,szabadSZF)]
```

```python
FGkond = FG[np.ix_(szabadSZF)]
```

```python
Umego = np.linalg.solve(KGkond,FGkond)
Umego
```

    array([ 0.00381972,  0.02015963,  0.01358122,  0.02122066, -0.0933709 ])

```python
UG = np.zeros(2*(Ne1+Ne2+1))
UG[np.ix_(szabadSZF)] += Umego
UG
```

    array([ 0.        ,  0.        ,  0.00381972,  0.02015963,  0.01358122,
            0.02122066,  0.        , -0.0933709 ])

```python
FR = np.dot(KG,UG)-FG
FR
```

    array([  1.25000000e+03,  -2.00000000e+02,   0.00000000e+00,
             9.09494702e-13,   0.00000000e+00,   5.25801624e-13,
             2.50000000e+02,   0.00000000e+00])

# Eredmények ábrázolása

```python
xiLista = np.linspace(-1,1,num = 10)
cspxKRD1=[[0 + i*a/Ne1,0 + (i+1)*a/Ne1] for i in range(Ne1)]
cspxKRD2=[[a + i*b/Ne2,a + (i+1)*b/Ne2] for i in range(Ne2)]
```

```python
x1Lista=[(cspxKRD1[eind][1]-cspxKRD1[eind][0])/2*(xi+1) + cspxKRD1[eind][0] for eind in range(Ne1) for xi in xiLista]
x2Lista=[(cspxKRD2[eind][1]-cspxKRD2[eind][0])/2*(xi+1) + cspxKRD2[eind][0] for eind in range(Ne2) for xi in xiLista]
xLista = np.concatenate((x1Lista,x2Lista))
```

```python
v1Lista = [np.dot(Nvfgv(xi,a/Ne1), UG[elemSZF1[eind]]) for eind in range(Ne1) for xi in xiLista]
v2Lista = [np.dot(Nvfgv(xi,b/Ne2), UG[elemSZF2[eind]]) for eind in range(Ne2) for xi in xiLista]
vLista = np.concatenate((v1Lista,v2Lista))
```

```python
figv = plt.figure(num = 1, figsize=(16/2.54,10/2.54))
axv = figv.add_subplot(111)

axv.plot(xLista,vLista)

plt.xlabel(r"$x \, \left[\mathrm{m}\right]$")
plt.ylabel(r"$v \, \left[\mathrm{m}\right]$")

plt.grid()
plt.legend()
plt.show()
```

```python
fi1Lista = [np.dot(Nfifgv(xi,a/Ne1),UG[elemSZF1[eind]]) for eind in range(Ne1) for xi in xiLista]
fi2Lista = [np.dot(Nfifgv(xi,b/Ne2),UG[elemSZF2[eind]]) for eind in range(Ne2) for xi in xiLista]
fiLista = np.concatenate((fi1Lista,fi2Lista))
```

```python
figfi = plt.figure(num = 2, figsize=(16/2.54,10/2.54))
axfi = figfi.add_subplot(111)

axfi.plot(xLista,fiLista)

plt.xlabel(r"$x \, \left[\mathrm{m}\right]$")
plt.ylabel(r"$\varphi \, \left[\mathrm{rad}\right]$")

plt.grid()
plt.legend()
plt.show()
```

```python
Mh1Lista = [-I1*E1*np.dot(Bvfgv(xi,a/Ne1),UG[elemSZF1[eind]]) for eind in range(Ne1) for xi in xiLista]
Mh2Lista = [-I2*E2*np.dot(Bvfgv(xi,b/Ne2),UG[elemSZF2[eind]]) for eind in range(Ne2) for xi in xiLista]
MhLista = np.concatenate((Mh1Lista,Mh2Lista))
```

```python
figMh = plt.figure(num = 3, figsize=(16/2.54,10/2.54))
axMh = figMh.add_subplot(111)

axMh.plot(xLista,MhLista)

plt.xlabel(r"$x \, \left[\mathrm{m}\right]$")
plt.ylabel(r"$M_h \, \left[\mathrm{Nm}\right]$")

plt.grid()
plt.legend()
plt.show()
```

```python
V1Lista = [I1*E1*np.dot(dddNvfgv(xi,a/Ne1),UG[elemSZF1[eind]]) for eind in range(Ne1) for xi in xiLista]
V2Lista = [I2*E2*np.dot(dddNvfgv(xi,b/Ne2),UG[elemSZF2[eind]]) for eind in range(Ne2) for xi in xiLista]
VLista = np.concatenate((V1Lista,V2Lista))
```

```python
figV = plt.figure(num = 4, figsize=(16/2.54,10/2.54))
axV = figV.add_subplot(111)

axV.plot(xLista,VLista)

plt.xlabel(r"$x \, \left[\mathrm{m}\right]$")
plt.ylabel(r"$V \, \left[\mathrm{N}\right]$")

plt.grid()
plt.legend()
plt.show()
```


# A-orthogonalization Process

$
\begin{align}
d_1 . d_2 &= u_1 . (u_2 - \beta_{21} u_1) \\
&= u_1 . (u_2 - \frac{u_2^T A u_1}{u_1^T A u_1} u_1) \\
&= 0
\end{align}
$

```julia
using LinearAlgebra

function GramSchmidt(A)
    D = A
    for i = 2:size(A,2)
        for j=1:(i-1)
            α = D[:,j]' * A * D[:,j]
            β = A[:,i]' * A * D[:,j]
            D[:,i] -= D[:,j]*β/α
        end
    end
    return D
end
```

    GramSchmidt (generic function with 1 method)

# Test

```julia
# SPD matrix
function spd(n)
    A = rand(n,n)
    vec,val = eigen(A)
    A = (A+A')/2
    A -= val[1]*I
end

A = spd(3)
```

    3×3 Array{Float64,2}:
     1.58094   0.253583  0.852915
     0.253583  1.33484   0.323033
     0.852915  0.323033  1.63285

```julia
GramSchmidt(A)

res = D[:,1]'*A*D[:,2], D[:,1]'*A*D[:,3], D[:,2]'*A*D[:,3]
```

    (-5171.217791884284, 3.452288961951408e14, -7.008827414932269e16)

## Adjourn

```julia
using Dates
println("mahdiar")
Dates.format(now(), "Y/U/d HH:MM")
```

    mahdiar

    "2021/February/19 14:39"