# Problem 1: Simple Difference in Outcomes

## A:Read in the dataset

```python
#import libraries
import pandas as pd
import numpy as np

```

```python
from google.colab import drive
drive.mount('/content/drive')
```

    Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).

```python
#load and read dataset
df = pd.read_csv('/content/drive/MyDrive/25052021/Copy of week_1_potential_outcomes.csv')
df.head()
```

      Unnamed: 0
      near_chipotle
      production_treatment
      Y0
      Y1

      0
      0
      1
      1
      12.27
      23.25

      1
      1
      1
      1
      14.18
      25.77

      2
      2
      1
      1
      12.16
      19.16

      3
      3
      1
      1
      13.99
      24.38

      4
      4
      1
      1
      14.84
      25.21

```python
#drop UNnames: 0 column
df.drop(['Unnamed: 0'],1, inplace=True)
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1

      0
      1
      1
      12.27
      23.25

      1
      1
      1
      14.18
      25.77

      2
      1
      1
      12.16
      19.16

      3
      1
      1
      13.99
      24.38

      4
      1
      1
      14.84
      25.21

## B:Create a variable production_Y

```python
df['production_y'] = df[["Y0","Y1"]].mean(axis=1)

df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y

      0
      1
      1
      12.27
      23.25
      17.760

      1
      1
      1
      14.18
      25.77
      19.975

      2
      1
      1
      12.16
      19.16
      15.660

      3
      1
      1
      13.99
      24.38
      19.185

      4
      1
      1
      14.84
      25.21
      20.025

```python
#dataset statistical summary
df.describe()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y

      count
      40.00000
      40.00000
      40.000000
      40.000000
      40.000000

      mean
      0.50000
      0.50000
      12.227500
      17.385000
      14.806250

      std
      0.50637
      0.50637
      2.341234
      5.084314
      3.589432

      min
      0.00000
      0.00000
      8.570000
      8.300000
      9.150000

      25%
      0.00000
      0.00000
      10.330000
      13.467500
      11.770000

      50%
      0.50000
      0.50000
      11.860000
      16.830000
      14.300000

      75%
      1.00000
      1.00000
      14.585000
      21.457500
      18.250000

      max
      1.00000
      1.00000
      16.640000
      25.770000
      20.780000

## C: Calculate the Simple Difference In Outcomes

```python
production_treatment_before = df.query("production_treatment	==0 & production_treatment	==0")["production_y"].mean()

production_treatment_after = df.query("production_treatment	==1 & production_treatment	==1")["production_y"].mean()

production_treatment_after - production_treatment_before
```

## D:Calculate the Selection Bias

```python
%pip install mlxtend --upgrade
```

    Requirement already up-to-date: mlxtend in /usr/local/lib/python3.7/dist-packages (0.18.0)
    Requirement already satisfied, skipping upgrade: numpy>=1.16.2 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (1.19.5)
    Requirement already satisfied, skipping upgrade: pandas>=0.24.2 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (1.1.5)
    Requirement already satisfied, skipping upgrade: scikit-learn>=0.20.3 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (0.22.2.post1)
    Requirement already satisfied, skipping upgrade: setuptools in /usr/local/lib/python3.7/dist-packages (from mlxtend) (57.0.0)
    Requirement already satisfied, skipping upgrade: matplotlib>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (3.2.2)
    Requirement already satisfied, skipping upgrade: joblib>=0.13.2 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (1.0.1)
    Requirement already satisfied, skipping upgrade: scipy>=1.2.1 in /usr/local/lib/python3.7/dist-packages (from mlxtend) (1.4.1)
    Requirement already satisfied, skipping upgrade: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.24.2->mlxtend) (2.8.1)
    Requirement already satisfied, skipping upgrade: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.24.2->mlxtend) (2018.9)
    Requirement already satisfied, skipping upgrade: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=3.0.0->mlxtend) (0.10.0)
    Requirement already satisfied, skipping upgrade: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=3.0.0->mlxtend) (2.4.7)
    Requirement already satisfied, skipping upgrade: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=3.0.0->mlxtend) (1.3.1)
    Requirement already satisfied, skipping upgrade: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas>=0.24.2->mlxtend) (1.15.0)

```python
# estimate the bias and variance for a regression model
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
import mlxtend
from mlxtend.evaluate import bias_variance_decomp

# separate into inputs and outputs
data = df.values
X, y = data[:, :-1], data[:, -1]

# split the data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
# define the model
model = LinearRegression()
# estimate bias and variance
mse, bias, var = bias_variance_decomp(model, X_train, y_train, X_test, y_test, loss='mse', num_rounds=200, random_seed=42)
# summarize results
print('MSE: %.3f' % mse)
print('Bias: %.3f' % bias)
print('Variance: %.3f' % var)
```

    MSE: 0.000
    Bias: 0.000
    Variance: 0.000

## E:Calculate the Heterogeneous Treatment Effect Bias

```python
!pip install econml
```

    Requirement already satisfied: econml in /usr/local/lib/python3.7/dist-packages (0.11.1)
    Requirement already satisfied: graphviz in /usr/local/lib/python3.7/dist-packages (from econml) (0.10.1)
    Requirement already satisfied: lightgbm in /usr/local/lib/python3.7/dist-packages (from econml) (2.2.3)
    Requirement already satisfied: numba!=0.42.1 in /usr/local/lib/python3.7/dist-packages (from econml) (0.51.2)
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from econml) (1.19.5)
    Requirement already satisfied: sparse in /usr/local/lib/python3.7/dist-packages (from econml) (0.12.0)
    Requirement already satisfied: joblib>=0.13.0 in /usr/local/lib/python3.7/dist-packages (from econml) (1.0.1)
    Requirement already satisfied: statsmodels>=0.10 in /usr/local/lib/python3.7/dist-packages (from econml) (0.10.2)
    Requirement already satisfied: shap~=0.38.1 in /usr/local/lib/python3.7/dist-packages (from econml) (0.38.1)
    Requirement already satisfied: dowhy in /usr/local/lib/python3.7/dist-packages (from econml) (0.6)
    Requirement already satisfied: scipy>1.4.0 in /usr/local/lib/python3.7/dist-packages (from econml) (1.4.1)
    Requirement already satisfied: scikit-learn>0.22.0 in /usr/local/lib/python3.7/dist-packages (from econml) (0.22.2.post1)
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from econml) (1.1.5)
    Requirement already satisfied: llvmlite<0.35,>=0.34.0.dev0 in /usr/local/lib/python3.7/dist-packages (from numba!=0.42.1->econml) (0.34.0)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from numba!=0.42.1->econml) (57.0.0)
    Requirement already satisfied: patsy>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from statsmodels>=0.10->econml) (0.5.1)
    Requirement already satisfied: cloudpickle in /usr/local/lib/python3.7/dist-packages (from shap~=0.38.1->econml) (1.3.0)
    Requirement already satisfied: tqdm>4.25.0 in /usr/local/lib/python3.7/dist-packages (from shap~=0.38.1->econml) (4.41.1)
    Requirement already satisfied: slicer==0.0.7 in /usr/local/lib/python3.7/dist-packages (from shap~=0.38.1->econml) (0.0.7)
    Requirement already satisfied: networkx>=2.0 in /usr/local/lib/python3.7/dist-packages (from dowhy->econml) (2.5.1)
    Requirement already satisfied: pydot>=1.4 in /usr/local/lib/python3.7/dist-packages (from dowhy->econml) (1.4.2)
    Requirement already satisfied: sympy>=1.4 in /usr/local/lib/python3.7/dist-packages (from dowhy->econml) (1.7.1)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas->econml) (2.8.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->econml) (2018.9)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from patsy>=0.4.0->statsmodels>=0.10->econml) (1.15.0)
    Requirement already satisfied: decorator<5,>=4.3 in /usr/local/lib/python3.7/dist-packages (from networkx>=2.0->dowhy->econml) (4.4.2)
    Requirement already satisfied: pyparsing>=2.1.4 in /usr/local/lib/python3.7/dist-packages (from pydot>=1.4->dowhy->econml) (2.4.7)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.4->dowhy->econml) (1.2.1)

```python
from econml.dml import SparseLinearDML
from sklearn.preprocessing import PolynomialFeatures
est = SparseLinearDML(featurizer=PolynomialFeatures(degree=4, include_bias=True))
T=df.production_y
y=df.production_treatment
X=df[['production_treatment','Y1','Y0']]
W = df[['Y1','Y0']]
est.fit(y, T, X=X, W=W)
lb, ub = est.const_marginal_effect_interval(X, alpha=.05)
```

    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00171195109058897, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0028426326263093493, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0035021319629531256, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0011491916542254454, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0016053104997031387, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0018535453801264978, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.001963048092901931, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0019830434119578744, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0019473495945357966, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0018786446489023056, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0017918457110454034, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003811195445248994, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007220168322150666, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007390696647414097, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007333302451886015, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00040462965156695674, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0005294539858355929, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0013042087626746746, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.002389889847225324, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0032759733009098957, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003998036634677848, tolerance: 0.000495
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0005124715225977017, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0014185480699083208, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0014008426437306376, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.001366677617696599, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0013216864815820406, tolerance: 0.00034375000000000003
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.000442982513701784, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0014360887545927792, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.000714467701188326, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0015700551861992373, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.000648456181046253, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0024615612816167864, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0031426481052237998, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003503137691831304, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0036561637963425375, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0036774851928341734, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0036169283629734483, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003506906399453058, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0033683493603582915, tolerance: 0.0004
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.001240220418602167, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0006371244070142446, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0011927176027182185, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0011113870576526796, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0010474961288344797, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003464785137034576, tolerance: 0.00039375
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.025788574339539738, tolerance: 0.000495
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0003254585328735793, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0006970785482676156, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0011857255664274868, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0017830634955208202, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.002482099620541023, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0032768203776908367, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.004161920657417606, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.005132604995288892, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0058567315679638465, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.001582422591396071, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.012160993762427852, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.04952361352039747, tolerance: 9.288485552394693e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00021171456929403032, tolerance: 9.975350635964289e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.000192174416392632, tolerance: 9.975350635964289e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00017781607101419183, tolerance: 9.975350635964289e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00016643595153342794, tolerance: 9.975350635964289e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00015692039775450084, tolerance: 9.975350635964289e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00012726652260031113, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0003344823604932712, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.000397027582970777, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.002401441178125241, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.006430683067347398, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.009030847305509515, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.010494450656338805, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011278017608441049, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011679790888921349, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011870600669618925, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011944596884196912, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011913984190462634, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.010262543048338735, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.009317845682400072, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008762986168899145, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008425043866848392, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008208351441383432, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008059440722451072, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007948308701433926, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007858162763425047, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007779693571804025, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007707833193466185, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007639906281036057, tolerance: 7.971468931310864e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0003328404350120273, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0005268161064262422, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0005182724674831496, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0023026821504193995, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.006978862493961069, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.009746041434843145, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011141505080058955, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011802981269217527, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.012093187400243344, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.01219592688428306, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.01220174927751394, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.012155418789475347, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.012079620839092864, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011986386248214798, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011882526276920768, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.01131106631804013, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0076784113754848615, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.005832860775729198, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.004861334874937118, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00432864111348974, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.004022479521469835, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003835548750171225, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003711955694614133, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003622042819655502, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0035499754554718654, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.003487345171365397, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0034297671339634794, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0033750348080140435, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0033221095985257465, tolerance: 0.00010647494578725312
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0004326814284654201, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0005043192713124478, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00365562649072515, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007803848691369075, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.01006509236553943, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011163612265798317, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.011927718032345602, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.010201501255309053, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.009143268838682661, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008615567090462961, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008340440674409866, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008184097924931688, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008081281903929538, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.008000351679986806, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007926402912904806, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007852869559262232, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007777286741307177, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007699146019176761, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007618815147175884, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00753700133281443, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007454491527634777, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.007372033047717386, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.002981680788596841, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00022927937805194976, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0003175188709692023, tolerance: 0.00011137756106716346
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 8.879054233925992e-07, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.6754102070084595e-06, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.0501726520011955e-06, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 8.879054233925992e-07, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.6754102070084595e-06, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.0501726520011955e-06, tolerance: 3.04972876857674e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.09054886104491e-07, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2044356793918247e-05, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.911820326597061e-06, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.065385659499078e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2865429665061707e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001880979870660382, tolerance: 0.00010014063170442713
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0008907070657006369, tolerance: 0.00012016801449634507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 6.9787779148206e-05, tolerance: 4.619311109667524e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0004363560167782686, tolerance: 4.619311109667524e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.00014307237064428713, tolerance: 5.733928915337573e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.001216528439655154, tolerance: 5.733928915337573e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.09054886104491e-07, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2044356793918247e-05, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.911820326597061e-06, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.065385659499078e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2865429665061707e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0002585078045144601, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0022115466625222263, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001203484171993438, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.006014502672216239, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001476254893096121, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0012514275255802003, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001781289343654978, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0032264691304700202, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0706407656030219, tolerance: 0.020778367859199943
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0257054050862493, tolerance: 0.024874414260457892
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07253696087056838, tolerance: 0.024874414260457892
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.033789843243609674, tolerance: 0.009738690587237277
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.09054886104491e-07, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2044356793918247e-05, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.911820326597061e-06, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.065385659499078e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2865429665061707e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0002585078045144601, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0022115466625222263, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001203484171993438, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.006014502672216239, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001476254893096121, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0012514275255802003, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001781289343654978, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0032264691304700202, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.09988560073501418, tolerance: 0.050507199942482366
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0829743288298248, tolerance: 0.019218059383079106
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07654896246535792, tolerance: 0.023364576527515137
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.03561247565295034, tolerance: 0.009013494280732772
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 33.073371617014686, tolerance: 10.38816103885436
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 13.32690897992859, tolerance: 10.38816103885436
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 34.18054447393115, tolerance: 11.869371799997937
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 13.356374701845368, tolerance: 4.7099857250875665
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.562731962642374, tolerance: 4.7099857250875665
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.27820452044557, tolerance: 5.581863292966837
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.046224525285595, tolerance: 5.7344650128568775
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.729366992328096, tolerance: 2.235121637023192
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.09054886104491e-07, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2044356793918247e-05, tolerance: 1.6920132127891978e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 5.911820326597061e-06, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 3.065385659499078e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 2.2865429665061707e-05, tolerance: 2.2384840057549677e-07
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0002585078045144601, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0022115466625222263, tolerance: 8.365857367039999e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001203484171993438, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.006014502672216239, tolerance: 0.00010335202131331507
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001476254893096121, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0012514275255802003, tolerance: 3.846649727230391e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0001781289343654978, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0032264691304700202, tolerance: 5.005479821157034e-05
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.09988560073501418, tolerance: 0.050507199942482366
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0829743288298248, tolerance: 0.019218059383079106
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07654896246535792, tolerance: 0.023364576527515137
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.03561247565295034, tolerance: 0.009013494280732772
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 50.18578609734004, tolerance: 10.042704199856152
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 22.78556714379829, tolerance: 10.042704199856152
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 18.46683168904019, tolerance: 11.53990258373469
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 38.24354963576002, tolerance: 11.53990258373469
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 24.491742454920086, tolerance: 11.53990258373469
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 57.483194043767526, tolerance: 11.539902583734692
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 15.374121872281762, tolerance: 4.557384005197527
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 19.140671617682983, tolerance: 4.557384005197527
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.66773940932967, tolerance: 5.445193482029011
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 19.994496740817112, tolerance: 5.445193482029011
    Objective did not converge. You might want to increase the number of iterations. Duality gap: 4.891245580657767, tolerance: 2.1645780637236554

```python
from econml.dml import CausalForestDML
from sklearn.ensemble import GradientBoostingRegressor
est = CausalForestDML(model_y=GradientBoostingRegressor(),
                model_t=GradientBoostingRegressor())
est.fit(y, T, X=X, W=W)
lb, ub = est.const_marginal_effect_interval(X, alpha=.05)
```

```python
lb
```

    array([-0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174,
           -0.01037174, -0.01037174, -0.01037174, -0.01037174, -0.01037174])

```python
ub
```

    array([0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488,
           0.03305488, 0.03305488, 0.03305488, 0.03305488, 0.03305488])

## F:Combine (C,D,E) to calculate the Average Treatment Effect

```python
import patsy
# some data prep for later
formula = """
production_y ~ production_y +	Y0 + Y1
"""
production_y, X = patsy.dmatrices(formula, df, return_type="dataframe")
# some categories are empty after dropping rows will Null, drop now
X = X.loc[:, X.sum() > 0]
production_treatment = production_y.iloc[:, 0]
treatment_variable = "production_y"
treatment = X["production_y"]
Xl = X.drop([ "production_y"], axis=1)
#scale = bw.std()
#center = bw.mean()
loc_id = df.loc[X.index, "production_treatment"].astype("category")

import re
# remove [ ] from names for compatibility with xgboost
Xl = Xl.rename(columns=lambda x: re.sub('\[|\]','_',x))
```

```python
# Estimate average treatment effects
from statsmodels.iolib.summary2 import summary_col
tmp = pd.DataFrame(dict(birthweight=production_treatment,treatment=treatment,assisted_delivery=df.loc[X.index, "production_treatment"]))
usage = smf.ols("assisted_delivery ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
health= smf.ols("production_y ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
summary_col([usage, health])
```

      	      assisted_delivery	 production_y

  Intercept	      -1.3779	         0.0000

  	              (0.1823)	        (0.0000)

  treatment	      0.1268	          1.0000

  	              (0.0058)	        (0.0000)

# Problem 2: Simulate an Experiment

## A: Create a new variable experiment_treatment

```python
# df['experiment_treatment'] = (-df.production_treatment.isin(range(0,1))).astype(int)
# df
from pandas import DataFrame
from numpy.random import randint
df['experiment_treatment'] = randint(2, size=40)
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y
      experiment_treatment

      0
      1
      1
      12.27
      23.25
      17.760
      1

      1
      1
      1
      14.18
      25.77
      19.975
      0

      2
      1
      1
      12.16
      19.16
      15.660
      1

      3
      1
      1
      13.99
      24.38
      19.185
      0

      4
      1
      1
      14.84
      25.21
      20.025
      1

## B: Create a new variable experiment_y

```python
import random
df['experiment_y']= np.random.randint(9.0,21.0, size=len(df))
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y
      experiment_treatment
      experiment_y
      permuted_treatment

      0
      1
      1
      12.27
      23.25
      17.760
      1
      13
      1

      1
      1
      1
      14.18
      25.77
      19.975
      0
      11
      1

      2
      1
      1
      12.16
      19.16
      15.660
      1
      13
      1

      3
      1
      1
      13.99
      24.38
      19.185
      0
      16
      0

      4
      1
      1
      14.84
      25.21
      20.025
      1
      14
      0

## C: Calculate the average treatment effect

```python
import patsy
from sklearn import linear_model, ensemble, base, neural_network
import statsmodels.formula.api as smf
import statsmodels.api as sm
from sklearn.utils.testing import ignore_warnings
from sklearn.exceptions import ConvergenceWarning

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
```

```python
# some data prep for later
formula = """
production_y ~ experiment_treatment +	experiment_y

"""
production_y, X = patsy.dmatrices(formula, df, return_type="dataframe")
# some categories are empty after dropping rows will Null, drop now
X = X.loc[:, X.sum() > 0]
production_y = production_y.iloc[:, 0]
treatment_variable = "experiment_y"
treatment = X["experiment_y"]
Xl = X.drop([ "experiment_y"], axis=1)
#scale = bw.std()
#center = bw.mean()
loc_id = df.loc[X.index, "experiment_treatment"].astype("category")

import re
# remove [ ] from names for compatibility with xgboost
Xl = Xl.rename(columns=lambda x: re.sub('\[|\]','_',x))
```

```python
# Estimate average treatment effects
from statsmodels.iolib.summary2 import summary_col
tmp = pd.DataFrame(dict(birthweight=production_y,treatment=treatment,assisted_delivery=df.loc[X.index, "experiment_y"]))
usage = smf.ols("assisted_delivery ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
health= smf.ols("production_y ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
summary_col([usage, health])
```

      	      assisted_delivery	 production_y

  Intercept	      0.0000	          19.7702

  	              (0.0000)	        (3.6564)

  treatment	      1.0000	          -0.3453

  	              (0.0000)	        (0.2417)

Problem 3: Permutation Testing

A: Create a new variable permuted_treatement with a 50% of being treated and a 50% chance of not being treated

```python
# df
from pandas import DataFrame
from numpy.random import randint
df['permuted_treatment'] = randint(2, size=40)
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y
      experiment_treatment
      experiment_y
      permuted_treatment

      0
      1
      1
      12.27
      23.25
      17.760
      1
      10
      1

      1
      1
      1
      14.18
      25.77
      19.975
      0
      11
      1

      2
      1
      1
      12.16
      19.16
      15.660
      1
      9
      1

      3
      1
      1
      13.99
      24.38
      19.185
      0
      11
      0

      4
      1
      1
      14.84
      25.21
      20.025
      1
      14
      0

B. Sharp Null Hypothesis.

Experiment y

```python
import random
df['experiment_y']= np.random.randint(0.0,1.0, size=len(df))
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y
      experiment_treatment
      experiment_y
      permuted_treatment

      0
      1
      1
      12.27
      23.25
      17.760
      1
      0
      1

      1
      1
      1
      14.18
      25.77
      19.975
      0
      0
      1

      2
      1
      1
      12.16
      19.16
      15.660
      1
      0
      1

      3
      1
      1
      13.99
      24.38
      19.185
      0
      0
      0

      4
      1
      1
      14.84
      25.21
      20.025
      1
      0
      0

```python
# some data prep for later
formula = """
production_y ~ permuted_treatment +	experiment_y

"""
production_y, X = patsy.dmatrices(formula, df, return_type="dataframe")
# some categories are empty after dropping rows will Null, drop now
X = X.loc[:, X.sum() > 0]
production_y = production_y.iloc[:, 0]
treatment_variable = "experiment_y"
treatment = X["experiment_y"]
Xl = X.drop([ "experiment_y"], axis=1)
#scale = bw.std()
#center = bw.mean()
loc_id = df.loc[X.index, "experiment_treatment"].astype("category")

import re
# remove [ ] from names for compatibility with xgboost
Xl = Xl.rename(columns=lambda x: re.sub('\[|\]','_',x))
```

```python
# Estimate average treatment effects
from statsmodels.iolib.summary2 import summary_col
tmp = pd.DataFrame(dict(birthweight=production_y,treatment=treatment,assisted_delivery=df.loc[X.index, "experiment_y"]))
usage = smf.ols("assisted_delivery ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
health= smf.ols("production_y ~ treatment", data=tmp).fit(cov_type="cluster", cov_kwds={'groups':loc_id})
summary_col([usage, health])
```

      	      assisted_delivery	 production_y

  Intercept	      -0.0000	         18.5975

  	              (0.0000)	        (1.2130)

  treatment	      1.0000	          -0.2557

  	              (0.0000)	        (0.0781)

C: Repeat this 1000 times. What is the average of the simulated average treatment effect under the null hypothesis. Plot a histogram of the outcomes

```python
#production_y ~ permuted_treatment +	experiment_y
df['outcomes']=production_y
df.head()
```

      near_chipotle
      production_treatment
      Y0
      Y1
      production_y
      experiment_treatment
      experiment_y
      permuted_treatment
      outcomes

      0
      1
      1
      12.27
      23.25
      17.760
      1
      13
      1
      17.760

      1
      1
      1
      14.18
      25.77
      19.975
      0
      11
      1
      19.975

      2
      1
      1
      12.16
      19.16
      15.660
      1
      13
      1
      15.660

      3
      1
      1
      13.99
      24.38
      19.185
      0
      16
      0
      19.185

      4
      1
      1
      14.84
      25.21
      20.025
      1
      14
      0
      20.025

```python
df.hist(column='outcomes')
```


```python
# HIDDEN
from datascience import *
from prob140 import *
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
%matplotlib inline
import math
from scipy import stats
from scipy import misc
```

```python
# HIDDEN
s = np.arange(1, 6)
def ref_walk_probs(i, j):
    if i-j == 0:
        return 0.5
    elif 2 <= i <= 4:
        if abs(i-j) == 1:
            return 0.25
        else:
            return 0
    elif i == 1:
        if j == 2:
            return 0.5
        else:
            return 0
    elif i == 5:
        if j == 4:
            return 0.5
        else:
            return 0

trans_tbl = Table().states(s).transition_function(ref_walk_probs)
refl_walk = trans_tbl.toMarkovChain()
```

### Examples ###
Here are two examples that make use of much of what you have learned in the last couple of chapters.

### A Diffusion Model by Ehrenfest ###
[Paul Ehrenfest](https://en.wikipedia.org/wiki/Paul_Ehrenfest) proposed a number of models for the diffusion of gas particles, one of which we will study here.

The model says that there are two containers containing a total of $N$ particles. At each instant, a container is selected at random and a particle is selected at random independently of the container. Then the selected particle is placed in the selected container; if it was already in that container, it stays there.

Let $X_n$ be the number of particles in Container 1 at time $n$. Then $X_0, X_1, \ldots$ is a Markov chain with transition probabilities given by:

\begin{equation}
P(i, j) =
 \begin{cases}
      \frac{N-i}{2N} & \text{if } j = i+1 \\
      \frac{1}{2} & \text{if } j = i \\
      \frac{i}{2N} & \text{if } j = i-1 \\
      0 & \text{otherwise}
   \end{cases}
\end{equation}

The chain is clearly irreducible. It is aperiodic because $P(i, i) > 0$.

**Question.** What is the stationary distribution of the chain?

**Answer.** We have computers. So let's first find the stationary distribution for $N=100$ particles, and then see if we can identify it for general $N$.

```python
N = 100

s = np.arange(N+1)

def trans_probs(i, j):
    if j == i:
        return 1/2
    if j == i+1:
        return (N-i)/(2*N)
    elif j == i-1:
        return i/(2*N)
    else:
        return 0

tbl = Table().states(s).transition_function(trans_probs)
ehrenfest = tbl.toMarkovChain()
Plot(ehrenfest.steady_state(), edges=True)
```

That looks suspiciously like the binomial (100, 1/2) distribution. In fact it *is* the binomial (100, 1/2) distribution. Since you've guessed it, all you have to do is plug it into the balance equations and check that they work out.

The balance equations are:

\begin{align*}
\pi(0) &= \frac{1}{2}\pi(0) + \frac{1}{2N}\pi(1) \\
\pi(j) &= \frac{N-(j-1)}{2N}\pi(j-1) + \frac{1}{2}\pi(j) + \frac{j+1}{2N}\pi(j+1), ~~~ 1 \le j \le N-1 \\
\pi(N) &= \frac{1}{2N}\pi(N-1) + \frac{1}{2}\pi(N)
\end{align*}

You have already guessed the solution by looking at the answer calculated for $N=100$. But if you want to start from scratch, you'll have to simplify the balance equations and try to write all the elements of $\pi$ in terms of $\pi(0)$. You will get:

\begin{align*}
\pi(1) &= N\pi(0) \\ \\
\pi(2) &= \frac{N(N-1)}{2} \pi0 = \binom{N}{2} \pi(0)
\end{align*}

and so on by induction:

$$
\pi(j) = \binom{N}{j} \pi(0)
$$

In other words, the stationary distribution is proportional to the binomial coefficients. So $\pi(0) = 1/2^n$ to make all the elements sum to 1, and the distribution is binomial $(N, 1/2)$.

### Expected Reward ###
Suppose I run the lazy reflecting random walk from the previous section for a long time. As a reminder, here is its stationary distribution.

```python
stationary = refl_walk.steady_state()
stationary
```

            State	 Probability

            1    	 0.125

            2    	 0.25

            3    	 0.25

            4    	 0.25

            5    	 0.125

**Question 1.** Suppose that every time the chain is in state 4, I win $\$4$; every time it's in state 5, I win $\$5$; otherwise I win nothing. What is my expected long run average reward?

**Answer 1.** In the long run, the chain is in steady state. So I expect that on 62.5% of the moves I will win nothing; on 25% of the moves I will win $\$4$; and on 12.5% of the moves I will win $\$5$. My expected long run average reward per move is $\$1.625$.

```python
0*0.625 + 4*0.25 + 5*.125
```

    1.625

**Question 2.** Suppose that every time the chain is in state $i$, I toss $i$ coins and record the number of heads. In the long run, how many heads do I expect to get on average per move?

**Answer 2.** Each time the chain is in state $i$, I expect to get $i/2$ heads. When the chain is in steady state, the expected number of coins I toss at any given move is 3. So, by iterated expectations, the long run average number of heads I expect to get is 1.5.

```python
stationary.expected_value()/2
```

    1.5000000000000002

If that seems artificial, consider this: Suppose I play the game above, and on every move I tell you the number of heads that I get *but I don't tell you which state the chain is in.* I *hide* the underlying Markov Chain. If you try to recreate the sequence of steps that the Markov Chain took, you are working with a Hidden Markov Model. These are much used in pattern recognition, bioinformatics, and other fields.

```python

```


Simulación matemática 2018

Lázaro Alonso

Email:   `alonsosilva@iteso.mx, lazarus.alon@gmail.com`

< [git GitHub tutorial 2](Clase2_GitTutorial2.ipynb) | [Guía](Clase0_GuiaSimulacionM.ipynb) | [Programación Lineal](Clase5_ProgramacionLineal.ipynb) >
___

### Optimización de funciones escalares diferenciables con `SymPy`

> - Mediante optimización se obtienen soluciones elegantes tanto en teoría como en ciertas aplicaciones.
> - La teoría de optimización usa elementos comenzando con cálculo elemental y álgebra lineal básica, y luego se extiende con análisis funcional y convexo.
> - Las aplicaciones en optimización involucran ciencia, ingeniería, economía, finanzas e industria.
> - El amplio y creciente uso de la optimización lo hace escencial para estudiantes y profesionales de cualquier rama de la ciencia y la tecnología.

**Referencia:**
- http://www.math.uwaterloo.ca/~hwolkowi//henry/reports/talks.d/t06talks.d/06msribirs.d/optimportance.shtml

Algunas aplicaciones son:

1. Ingeniería
 - Encontrar la composición de equilibrio de una mezcla de diferentes átomos.
 - Planeación de ruta para un robot (o vehículo aéreo no tripulado).
 - Planeación de la mano de obra óptima en una construcción o planta de producción.
2. Distribución óptima de recursos.
 - Distribución de rutas de vuelo.
 - Encontrar una dieta óptima.
 - Planeación de ruta óptima.
3. Optimización financiera
 - Administración de riesgos.
 - Portafolios de inversión.

En esta clase veremos aspectos básicos de optimización. En específico, veremos cómo obtener máximos y mínimos de una función escalar de una variable (como en cálculo diferencial).

___
## 0. Librerías que usaremos

Como ya dijimos en la primer clase `python` es el lenguaje de programación (el cual es de alto nivel). Sin embargo, `python` solo tiene unos pocos comandos primitivos y para hacer más fácil su uso en nuestras actividades de simulación en ingeniería, otras personas ya han escrito ciertas librerías por nosotros.

### 0.1 `NumPy`

`NumPy` (Numerical Python) es la librería fundamental para computación científica (numérica) con `Python`. Contiene, entre otras cosas:
- un objeto tipo arreglo N-dimensional muy poderoso
- funciones sofisticadas
- funciones de álgebra lineal, transformada de Fourier y números aleatorios.

Por lo anterior, `NumPy` es de amplio uso entre la comunidad científica e ingenieril (por su manejo de cantidades vectoriales). De la misma manera, se usa para guardar datos. Para nuestros propósitos, se puede usar libremente.

**Referencia:**
- http://www.numpy.org/

`NumPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería numpy
import numpy as np
```

### 0.2 `SymPy`

`SymPy` (Symbolic Python) es una librería de `Python` para matemáticas simbólicas. Su objetivo es convertirse en un sistema de álgebra computacional con las mejores características, manteniendo el código lo más simple posible para que sea comprensible.

**Referencia:**
- http://www.sympy.org/en/index.html

`SymPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería sympy
import sympy as sym
# imprimir en formato latex
sym.init_printing(use_latex='mathjax')
```

La funcionalidad de imprimir en formato LaTex que nos da `SymPy` mediante el proyecto `mathjax` hace de `SymPy` una herramienta muy atractiva...

Notar que en `SymPy` y en `NumPy` existen funciones con el mismo nombre, pero reciben tipos de datos diferentes...

Explicar el uso de la sintaxis `from numpy import *` y sus peligros (no recomendable).

```python
# diferencias de funciones de sympy y numpy
sym.var('x')
sym.sin(x)*np.sin(3)
```

$$0.141120008059867 \sin{\left (x \right )}$$

```python
# importar con * y ver que pasa
# from numpy import *
# from sympy import *
# No recomendado
```

### 0.3 `PyPlot` de `matplotlib`

El módulo `PyPlot` de la librería `matplotlib` contiene funciones que nos permite generar una gran cantidad de gráficas rápidamente. Las funciones de este módulo están escritas con el mismo nombre que las funciones para graficar en `Matlab`.

**Referencia:**
- https://matplotlib.org/api/pyplot_summary.html

```python
# importar matplotlib.pyplot
import matplotlib.pyplot as plt
# comando para que las gráficas salgan en la misma ventana
%matplotlib inline
```

```python
### Style, plots
```

```python

```

Ya que revisamos todas las librerías que usaremos, empecemos con la clase como tal...

___
Basamos todos los resultados en los siguientes teoremas:

## 1. Teorema de Fermat (análisis)

Si una función $f(x)$ alcanza un máximo o mínimo local en $x=c$, y si la derivada $f'(c)$ existe en el punto $c$, entonces $f'(c) = 0$.

### Ejemplo

Sabemos que la función $f(x)=x^2$ tiene un mínimo global en $x=0$, pues

$$f(x)=x^2\geq0,\qquad\text{y}\qquad f(x)=x^2=0 \qquad\text{si y solo si}\qquad x=0.$$

```python
# declarar la variable real x
#t = sym.Symbol('x')
#t
sym.var('x', real = True)
```

$$x$$

```python
# declarar ahora f=x^2 y mostrar
f = x**2
f
```

$$x^{2}$$

```python
# derivar f respecto a x y mostrar
df = sym.diff(f, x)
df
```

$$2 x$$

```python
# resolver f'(x)=0 y mostrar soluciones
xc = sym.solve(df, x)
xc
```

$$\left [ 0\right ]$$

Veamos la gráfica...

```python
# convertir f e una función que se pueda evaluar numéricamente
#(función lambdify de la librería sympy)
f_num = sym.lambdify([x], f, 'numpy')
x_vec = np.linspace(-5, 5, 100)

# graficar
plt.plot(x_vec, f_num(x_vec))
plt.xlabel('$x$')
plt.ylabel('$x^2$')
plt.show()
```

Ver diferencias entre f y f_num

```python
# intentar evaluar f y f_num

```

**Otra manera de hacer lo anterior**

Concepto de función...

```python
def f(x):
    return x**2
```

```python
f(x)
```

$$x^{2}$$

```python
f(np.array([5.124, 2.5436]))
```

    array([26.255376  ,  6.46990096])

```python
df = sym.diff(f(x), x)
df
```

$$2 x$$

```python
x_c = sym.solve(df, x)
x_c[0]
```

$$0$$

```python
# graficar
plt.plot(x_vec, f(x_vec))
plt.xlabel('$x$')
plt.ylabel('$x^2$')
plt.show()
```

El converso del teorema anterior no es cierto.

### Actividad
Considere $g(x)=x^3$.
- Usando `sympy`, muestre que $g'(0)=0$.
- Sin embargo, descartar que $x=0$ es un extremo de $g(x)$ viendo su **gráfica**.

```python
def g(x):
    return x**3
```

```python
dg = sym.diff(g(x), x)
dg
```

$$3 x^{2}$$

```python
puntos_criticos = sym.solve(dg, x)
puntos_criticos
```

$$\left [ 0\right ]$$

```python
dg_eval0 = dg.subs(x, puntos_criticos[0])
dg_eval0
```

$$0$$

```python
# graficar
plt.plot(x_vec, g(x_vec))
plt.xlabel('$x$')
plt.ylabel('$x^3$')
plt.show()
```

## 2. Criterio de la segunda derivada

Sea $f(x)$ una función tal que $f’(c)=0$ y cuya segunda derivada existe en un intervalo abierto que contiene a $c$.
- Si $f’’(c)>0$, entonces $f(c)$ es un mínimo relativo.
- Si $f’’(c)<0$, entonces $f(c)$ es un máximo relativo.
- Si $f’’(c)=0$, entonces el criterio no decide.

### Ejemplo

Mostrar, usando `sympy`, que la función $f(x)=x^2$ tiene un mínimo relativo en $x=0$.

Ya vimos que $f'(0)=0$. Notemos que:

```python
f = x**2
#d2f = sym.diff(f, x, x)
d2f = sym.diff(f, x, 2)
d2f
```

$$2$$

```python
d2f>0
```

$$\mathrm{True}$$

Por tanto, por el criterio de la segunda derivada, $f(0)=0$ es un mínimo relativo (en efecto, el mínimo global).

### Ejemplo
¿Qué pasa con $g(x)=x^3$ al intentar utilizar el criterio de la segunda derivada? (usar `sympy`).

```python
g(x)
```

$$x^{3}$$

```python
d2g = sym.diff(g(x), x, x)
d2g
```

$$6 x$$

```python
d2g.subs(x, 0)
```

$$0$$

### Actividad

¿Qué pasa con $h(x)=x^4$ al intentar utilizar el criterio de la segunda derivada?.

```python
def h(x):
    return x**4
```

```python
dh = sym.diff(h(x), x, 1)
dh
```

$$4 x^{3}$$

```python
p_c_h = sym.solve(dh, x)
p_c_h
```

$$\left [ 0\right ]$$

```python
d2h = sym.diff(h(x), x, 2)
d2h
```

$$12 x^{2}$$

```python
d2h.subs(x, p_c_h[0])
```

$$0$$

Como la segunda derivada en el punto crítico es cero, el criterio de la segunda derivada no decide.

## 3. Método para determinar extremos absolutos de una función continua y=f(x) en [a,b]

- Determinar todos los valores críticos $c_1, c_2, c_3, \dots, c_n$ en $(a,b)$.
- Evaluar $f$ en todos los valores críticos y en los extremos $x=a$ y $x=b$.
- El más grande y el más pequeño de los valores de la lista $f(a), f(b), f(c_1), f(c_2), \dots, f(c_n)$ son el máximo absoluto y el mínimo absoluto, respectivamente, de f en el intervalo [a,b].

### Ejemplo

Determinar los extremos absolutos de $f(x)=x^2-6x$ en $\left[0,5\right]$.

Obtenemos los puntos críticos de $f$ en $\left[0,5\right]$:

```python
f = x**2-6*x
f
```

$$x^{2} - 6 x$$

```python
df = sym.diff(f, x)
df
```

$$2 x - 6$$

```python
x_c = sym.solve(df, x)
x_c
```

$$\left [ 3\right ]$$

Evaluamos $f$ en los extremos y en los puntos críticos:

```python
f.subs(x, 0), f.subs(x, 5), f.subs(x, x_c[0])
```

$$\left ( 0, \quad -5, \quad -9\right )$$

Concluimos que el máximo absoluto de $f$ en $\left[0,5\right]$ es $0$ y se alcanza en $x=0$, y que el mínimo absoluto es $-9$ y se alcanza en $x=3$.

```python
f_num = sym.lambdify([x], f, 'numpy')
x_vec = np.linspace(0, 5, 100)

plt.figure(figsize=(8,6))
plt.plot(x_vec, f_num(x_vec), 'k--', label = '$y=f(x)$')
plt.plot([0], [0], '*r', label = '$(0,0=\max_{0\leq x\leq 5} f(x))$')
plt.plot([3], [-9], '*b', label = '$(3,-9=\min_{0\leq x\leq 5} f(x))$')
plt.legend(loc='best')
plt.xlabel('x')
plt.grid()
plt.show()
```

### Actividad
Determinar los valores extremos absolutos de $h(x)=x^3-3x$ en $\left[-2.2,1.8\right]$, usando `sympy`. Mostrar en una gráfica.

```python
sym.var('x', real = True)
```

$$x$$

```python
def h(x):
    return x**3-3*x
```

```python
dh = sym.diff(h(x), x)
dh
```

$$3 x^{2} - 3$$

```python
p_c = sym.solve(dh, x)
p_c
```

$$\left [ -1, \quad 1\right ]$$

```python
h(-1), h(1), h(-2.2), h(1.8)
```

$$\left ( 2, \quad -2, \quad -4.048000000000003, \quad 0.4320000000000004\right )$$

```python
x_vec = np.linspace(-2.2, 1.8, 50)

plt.figure(figsize=(7,5))
plt.plot(x_vec, h(x_vec), 'b:', label = '$y=x^3-3x$')
plt.plot([-2.2], [h(-2.2)], 'mo', label = '(-2.2,-4.048)')
plt.plot([-1], [h(-1)], 'yo', label = '(-1,2)')
plt.legend(loc='best')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.grid()
plt.show()
```

### En varias variables...

El procedimiento es análogo.

Si una función $f:\mathbb{R}^n\to\mathbb{R}$ alcanza un máximo o mínimo local en $\boldsymbol{x}=\boldsymbol{c}\in\mathbb{R}^n$, y $f$ es diferenciable en el punto $\boldsymbol{x}=\boldsymbol{c}$, entonces $\left.\frac{\partial f}{\partial \boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{c}}=\boldsymbol{0}$ (todas las derivadas parciales en el punto $\boldsymbol{x}=\boldsymbol{c}$ son cero).

**Criterio de la segunda derivada:** para ver si es máximo o mínimo, se toma la segunda derivada (matriz jacobiana) y se verifica definición negativa o positiva, respectivamente.

Si se restringe a cierta región, hay ciertas técnicas. La más general, pero también la más compleja es la de **multiplicadores de Lagrange**.

```python
sym.var('x y')
x, y
```

$$\left ( x, \quad y\right )$$

```python
def f(x, y):
    return x**2 + y**2
```

```python
dfx = sym.diff(f(x,y), x)
dfy = sym.diff(f(x,y), y)
dfx, dfy
```

$$\left ( 2 x, \quad 2 y\right )$$

```python
xy_c = sym.solve([dfx, dfy], [x, y])
xy_c
```

$$\left \{ x : 0, \quad y : 0\right \}$$

```python
x_c, y_c = xy_c[x], xy_c[y]
x_c, y_c
```

$$\left ( 0, \quad 0\right )$$

```python
d2fx = sym.diff(f(x,y), x, 2)
d2fy = sym.diff(f(x,y), y, 2)
dfxy = sym.diff(f(x,y), x, y)

Jf = sym.Matrix([[d2fx, dfxy], [dfxy, d2fy]])
Jf.eigenvals()
```

$$\left \{ 2 : 2\right \}$$

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

x = np.linspace(-2, 2, 100)
y = x
X, Y = np.meshgrid(x, y)

ax.plot_surface(X, Y, f(X, Y))
ax.plot([x_c], [y_c], [f(x_c,y_c)], '*r');
```

### Tareas.

1. Por parejas harán un proyecto colaborativo.
2. Introducción a numpy. Obtener máximos y mínimos absolutos, usando `sympy`, de las funciones dadas en los intervalos dados, y graficar la función en dicho intervalo señalando los puntos máximo y mínimo absolutos.

**Definir fecha de las tareas**

___

< [git GitHub tutorial 2](Clase2_GitTutorial2.ipynb) | [Guía](Clase0_GuiaSimulacionM.ipynb) | [Programación Lineal](Clase5_ProgramacionLineal.ipynb) >

Created with Jupyter by Lázaro Alonso.
  Copyright:  Public Domain como en [CC](https://creativecommons.org/licenses/by/2.0/) (Exepto donde se indique lo contrario)


# Neuromatch Academy: Week 1, Day 5, Tutorial 2
# Dimensionality Reduction: Principal component analysis

---

In this notebook we'll learn how to perform PCA by projecting the data onto the eigenvectors of its covariance matrix.

Steps:
 1. Calculate the eigenvectors of the sample covariance matrix.
 2. Perform PCA by projecting data onto the eigenvectors of the covariance matrix.
 3. Plot the eigenvalues.

---

```python
#@title Video: PCA
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="Ze0uK1ipEn0", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video

```

    Video available at https://youtube.com/watch?v=Ze0uK1ipEn0

# Setup
Run these cells to get the tutorial started.

```python
#library imports
import time                        # import time
import numpy as np                 # import numpy
import scipy as sp                 # import scipy
import math                        # import basic math functions
import random                      # import basic random number generator functions

import matplotlib.pyplot as plt    # import matplotlib
from IPython import display
```

```python
#@title Figure Settings
%matplotlib inline
fig_w, fig_h = (8, 8)
plt.rcParams.update({'figure.figsize': (fig_w, fig_h)})
plt.style.use('ggplot')
%config InlineBackend.figure_format = 'retina'
```

```python
#@title Helper functions
# new helper functions
def plot_eigenvalues(evals):
  """
  Plots eigenvalues.

  Args:
     (numpy array of floats) : Vector of eigenvalues

  Returns:
    Nothing.

  """
  plt.plot(np.arange(1,len(evals)+1),evals,'o-k')
  plt.xlabel('Component')
  plt.ylabel('Eigenvalue')
  plt.title('Scree plot')
  plt.xticks(np.arange(1,len(evals)+1))
  plt.ylim([0,2.5])

def sort_evals_descending(evals,evectors):
  """
  Sorts eigenvalues and eigenvectors in decreasing order. Also aligns first two
  eigenvectors to be in first two quadrants (if 2D).

  Args:
    evals (numpy array of floats):   Vector of eigenvalues
    evectors (numpy array of floats):   Corresponding matrix of eigenvectors
                                each column corresponds to a different eigenvalue

  Returns:
    (numpy array of floats) : Vector of eigenvalues after sorting
    (numpy array of floats) : Matrix of eigenvectors after sorting
  """
  index = np.flip(np.argsort(evals))
  evals = evals[index]
  evectors = evectors[:,index]
  if evals.shape[0] == 2:
    if np.arccos(np.matmul(evectors[:,0], 1./np.sqrt(2)*np.array([1,1]))) > np.pi/2.:
      evectors[:,0] = -evectors[:,0]
    if np.arccos(np.matmul(evectors[:,1], 1./np.sqrt(2)*np.array([-1,1]))) > np.pi/2.:
      evectors[:,1] = -evectors[:,1]
  return evals, evectors

  # Old helper functions from Tutorial 1

def plot_data(X):
  """
  Plots bivariate data. Includes a plot of each random variable, and a scatter
  scatter plot of their joint activity. The title indicates the sample correlation
  calculated from the data.

  Args:
    X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable

  Returns:
    Nothing.
  """
  fig = plt.figure(figsize=[8,4])
  gs = fig.add_gridspec(2,2)
  ax1 = fig.add_subplot(gs[0,0])
  ax1.plot(X[:,0],color='k')
  plt.ylabel('Neuron 1')
  ax2 = fig.add_subplot(gs[1,0])
  ax2.plot(X[:,1],color='k')
  plt.xlabel('Sample Number (sorted)')
  plt.ylabel('Neuron 2')
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(X[:,0],X[:,1],'.',markerfacecolor=[.5,.5,.5], markeredgewidth=0)
  ax3.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(X[:,0],X[:,1])[0,1]))

def get_data(cov_matrix):
  """
  Returns a matrix of 1000 samples from a bivariate, zero-mean Gaussian

  Note that samples are sorted in ascending order for the first random variable.

  Args:
    var_1 (scalar):    variance of the first random variable
    var_2 (scalar):    variance of the second random variable
    cov_matrix (numpy array of floats): desired covariance matrix

  Returns:
    (numpy array of floats) : samples from the bivariate Gaussian, with
                              each column corresponding to a different random variable
  """
  mean = np.array([0,0])
  X = np.random.multivariate_normal(mean,cov_matrix,size = 1000)
  indices_for_sorting = np.argsort(X[:,0])
  X = X[indices_for_sorting,:]
  return X

def calculate_cov_matrix(var_1,var_2,corr_coef):
  """
  Calculates the covariance matrix based on the variances and correlation coefficient.

  Args:
    var_1 (scalar):    variance of the first random variable
    var_2 (scalar):    variance of the second random variable
    corr_coef (scalar):      correlation coefficient

  Returns:
    (numpy array of floats) : covariance matrix
  """
  cov = corr_coef * np.sqrt(var_1 * var_2)
  cov_matrix = np.array([[var_1,cov],[cov,var_2]])
  return cov_matrix

def define_orthonormal_basis(u):
  """
  Calculates an orthonormal basis given an arbitrary vector u.

  Args:
    u (numpy array of floats):    arbitrary 2D vector used for new basis

  Returns:
    (numpy array of floats) : new orthonormal basis
                                columns correspond to basis vectors
  """
  u = u / np.sqrt(u[0]**2 + u[1]**2)
  w = np.array([-u[1],u[0]])
  W = np.column_stack((u,w))
  return W

def plot_data_new_basis(Y):
  """
  Plots bivariate data after transformation to new bases. Similar to plot_data but
  with colors corresponding to projections onto basis 1 (red) and basis 2 (blue).
  The title indicates the sample correlation calculated from the data.

  Note that samples are re-sorted in ascending order for the first random variable.

  Args:
    Y (numpy array of floats):   Data matrix in new basis
                                  each column corresponds to a different random variable

  Returns:
    Nothing.
  """
  fig = plt.figure(figsize=[8,4])
  gs = fig.add_gridspec(2,2)
  ax1 = fig.add_subplot(gs[0,0])
  ax1.plot(Y[:,0],'r')
  plt.ylabel('Projection \n basis vector 1')
  ax2 = fig.add_subplot(gs[1,0])
  ax2.plot(Y[:,1],'b')
  plt.xlabel('Projection \n basis vector 1')
  plt.ylabel('Projection \n basis vector 2')
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(Y[:,0],Y[:,1],'.',color=[.5,.5,.5])
  ax3.axis('equal')
  plt.xlabel('Projection basis vector 1')
  plt.ylabel('Projection basis vector 2')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(Y[:,0],Y[:,1])[0,1]))

def change_of_basis(X,W):
  """
  Projects data onto a new basis.

  Args:
    X (numpy array of floats) : Data matrix
                                each column corresponding to a different random variable
    W (numpy array of floats):  new orthonormal basis
                                columns correspond to basis vectors

  Returns:
    (numpy array of floats) : Data matrix expressed in new basis
  """
  Y = np.matmul(X,W)
  return Y

def plot_basis_vectors(X,W):
  """
  Plots bivariate data as well as new basis vectors.

  Args:
    X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable
    W (numpy array of floats):   Square matrix representing new orthonormal basis
                                  each column represents a basis vector

  Returns:
    Nothing.
  """
  plt.figure(figsize=[4,4])
  plt.plot(X[:,0],X[:,1],'.',color=[.5,.5,.5],label='Data')
  plt.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.plot([0,W[0,0]],[0,W[1,0]],color='r',linewidth=3,label = 'Basis vector 1')
  plt.plot([0,W[0,1]],[0,W[1,1]],color='b',linewidth=3,label = 'Basis vector 2')
  plt.legend()

```

# Calculate the eigenvectors of the the sample covariance matrix.

PCA represents data in a new basis defined by the eigenvectors of the covariance matrix. In the previous tutorial, we generated bivariate normal data with a specified covariance matrix $\bf \Sigma$, whose $(i,j)$th element is:
\begin{equation}
\Sigma_{ij} = E[ x_i x_j ] - E[ x_i] E[ x_j ] .
\end{equation}
However, in real life we don't have access to the ground-truth covariance matrix. To get around this, we can use the sample covariance matrix, $\bf\hat\Sigma$, calculated directly from the data. The $(i,j)$th element of the sample covariance matrix is:
\begin{equation}
 \hat \Sigma_{ij} =  \frac{1}{N_\text{samples}}{\bf x}_i^T {\bf x}_j - \bar {\bf x}_i \bar{\bf x}_j ,
\end{equation}
where ${\bf x}_i = [ x_i(1), x_i(2), \dots,x_i(N_\text{samples})]^T$ is a column vector representing all measurements of neuron i, and  $\bar {\bf x}_i$ is a scalar representing its sample mean:
\begin{equation}
\bar {\bf x}_i = \frac{1}{N_\text{samples}} \sum_{k=1}^{N_\text{samples}} x_i(k).
\end{equation}
If we assume that the data has already been mean-subtracted, then we can write:
\begin{align}
{\bf \hat \Sigma}
&= \frac{1}{N_\text{samples}} {\bf X}^T {\bf X}.
\end{align}

---

#### Exercise

Before we move on to calculating the eigenvectors, we'll first calculate the sample covariance matrix from the data. Your job is to finish the function `get_sample_cov_matrix`. Then you can use the provided code to generate bivariate normal data with `get_data`, and calculate the sample covariance matrix with your finished `get_sample_cov_matrix`. Then you can compare this estimate to the true covariate matrix using `calculate_cov_matrix`,

```python
help(get_data)
help(calculate_cov_matrix)
```

    Help on function get_data in module __main__:

    get_data(cov_matrix)
        Returns a matrix of 1000 samples from a bivariate, zero-mean Gaussian

        Note that samples are sorted in ascending order for the first random variable.

        Args:
          var_1 (scalar):    variance of the first random variable
          var_2 (scalar):    variance of the second random variable
          cov_matrix (numpy array of floats): desired covariance matrix

        Returns:
          (numpy array of floats) : samples from the bivariate Gaussian, with
                                    each column corresponding to a different random variable

    Help on function calculate_cov_matrix in module __main__:

    calculate_cov_matrix(var_1, var_2, corr_coef)
        Calculates the covariance matrix based on the variances and correlation coefficient.

        Args:
          var_1 (scalar):    variance of the first random variable
          var_2 (scalar):    variance of the second random variable
          corr_coef (scalar):      correlation coefficient

        Returns:
          (numpy array of floats) : covariance matrix

```python
def get_sample_cov_matrix(X):
  """
    Returns the sample covariance matrix of data X

    Args:
      X (numpy array of floats):   Data matrix
                                   each column corresponds to a different random variable

    Returns:
      (numpy array of floats) : Covariance matrix
  """
  ###################################################################
  ## Insert your code here to:
  ##                Subtract the mean of X
  ##                Calculate the covariance matrix (hint: use np.matmul)

  #X = ...
  #cov_matrix = ...

  #uncomment once you've filled in the function
  raise NotImplementedError("Student excercise: calculate the covariance matrix!")
  ###################################################################
  return cov_matrix

## Uncomment below code to:
##                generate bivariate Gaussian data with variances of 1
##                and a correlation coefficient of 0.8
##                compare the true and sample covariance matrices
###################################################################
variance_1 = 1
variance_2 = 1
corr_coef = 0.8

#cov_matrix = calculate_cov_matrix(variance_1,variance_2,corr_coef)
#print(cov_matrix)

#X = get_data(cov_matrix)
#sample_cov_matrix = get_sample_cov_matrix(X)
#print(sample_cov_matrix)

```

```python
# to_remove solution

def get_sample_cov_matrix(X):
  """
    Returns the sample covariance matrix of data X

    Args:
      X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable

    Returns:
      (numpy array of floats) : Covariance matrix
  """
  X = X - np.mean(X,0)
  cov_matrix = 1./X.shape[0]*np.matmul(X.T,X)
  return cov_matrix

cov_matrix = calculate_cov_matrix(variance_1,variance_2,corr_coef)
print(cov_matrix)

X = get_data(cov_matrix)
sample_cov_matrix = get_sample_cov_matrix(X)
print(sample_cov_matrix)

```

    [[1.  0.8]
     [0.8 1. ]]
    [[1.04845333 0.86773878]
     [0.86773878 1.06646279]]

#### Exercise
Next we will compare the eigenvectors of the covariance matrix to the geometrical view of the data.

**Suggestions:**
* Calculate the eigenvalues and eigenvectors of the sample covariance matrix. (**Hint:** use `np.linalg.eig`).
* Use the provided code to sort the eigenvalues in descending order.
* Plot the eigenvectors on a scatter plot of the data, using the function `plot_basis_vectors`.

```python
help(sort_evals_descending)
help(plot_basis_vectors)
```

    Help on function sort_evals_descending in module __main__:

    sort_evals_descending(evals, evectors)
        Sorts eigenvalues and eigenvectors in decreasing order. Also aligns first two
        eigenvectors to be in first two quadrants (if 2D).

        Args:
          evals (numpy array of floats):   Vector of eigenvalues
          evectors (numpy array of floats):   Corresponding matrix of eigenvectors
                                      each column corresponds to a different eigenvalue

        Returns:
          (numpy array of floats) : Vector of eigenvalues after sorting
          (numpy array of floats) : Matrix of eigenvectors after sorting

    Help on function plot_basis_vectors in module __main__:

    plot_basis_vectors(X, W)
        Plots bivariate data as well as new basis vectors.

        Args:
          X (numpy array of floats):   Data matrix
                                        each column corresponds to a different random variable
          W (numpy array of floats):   Square matrix representing new orthonormal basis
                                        each column represents a basis vector

        Returns:
          Nothing.

```python
###################################################################
## Insert your code here to:
##                Calculate the eigenvalues and eigenvectors
##                Sort the eigenvalues in descending order
##                plot the eigenvectors against the data
###################################################################

# evals, evectors = ...
# evals, evectors = ..
# plot_basis_vectors(X,evectors)

```

```python
# to_remove solution

evals, evectors = np.linalg.eig(cov_matrix)
evals, evectors = sort_evals_descending(evals,evectors)

with plt.xkcd():
  plot_basis_vectors(X,evectors)

```

# Perform PCA by projecting data onto the eigenvectors.

To perform PCA, we will project the data onto the eigenvectors of the covariance matrix, i.e.:
\begin{equation}
\bf S = X W
\end{equation}
where $\bf S$ is an $N_\text{samples} \times N$ matrix representing the projected data (also called *scores*), and $W$ is an $N\times N$ orthogonal matrix, each of whose columns represents the eigenvectors of the covariance matrix (also called *weights* or *loadings*).

#### Exercise
We will now perform PCA on the data using the intuition we have built. Fill in the function below to carry out the steps to perform PCA by projecting the data onto the eigenvectors of its covariance matrix.

**Suggestions**
* First subtract the mean.
* Then calculate the covariance.
* Then find the eigenvalues and order them in descending order.
* Finally project the mean-centered data onto the eigenvectors.

```python
help(change_of_basis)
help(plot_data_new_basis)
```

    Help on function change_of_basis in module __main__:

    change_of_basis(X, W)
        Projects data onto a new basis.

        Args:
          X (numpy array of floats) : Data matrix
                                      each column corresponding to a different random variable
          W (numpy array of floats):  new orthonormal basis
                                      columns correspond to basis vectors

        Returns:
          (numpy array of floats) : Data matrix expressed in new basis

    Help on function plot_data_new_basis in module __main__:

    plot_data_new_basis(Y)
        Plots bivariate data after transformation to new bases. Similar to plot_data but
        with colors corresponding to projections onto basis 1 (red) and basis 2 (blue).
        The title indicates the sample correlation calculated from the data.

        Note that samples are re-sorted in ascending order for the first random variable.

        Args:
          Y (numpy array of floats):   Data matrix in new basis
                                        each column corresponds to a different random variable

        Returns:
          Nothing.

```python
def pca(X):
  """
  Sorts eigenvalues and eigenvectors in decreasing order.

  Args:
     X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable

  Returns:
    (numpy array of floats) : Data projected onto the new basis
    (numpy array of floats) : Vector of eigenvalues
    (numpy array of floats) : Corresponding matrix of eigenvectors

  """
  ###################################################################
  ## Insert your code here to ...
  ##                Subtract the mean of X
  ##                Calculate the sample covariance matrix
  ##                Calculate the eigenvalues and eigenvectors
  ##                Sort the eigenvalues in descending order
  ##                Project the data onto the new eigenvector basis
  ###################################################################
  return score, evectors, evals

###################################################################
## Insert your code here to:
##                perform PCA on the data matrix X
##                Plot the data projected into the new basis
###################################################################

# score, evectors, evals = ...
# plot_data_new_basis(score)
```

```python
# to_remove solution

def pca(X):
  """
  Performs PCA on multivariate data.

  Args:
     X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable

  Returns:
    (numpy array of floats) : Data projected onto the new basis
    (numpy array of floats) : Vector of eigenvalues
    (numpy array of floats) : Corresponding matrix of eigenvectors

  """
  X = X - np.mean(X,0)
  cov_matrix = get_sample_cov_matrix(X)
  evals, evectors = np.linalg.eig(cov_matrix)
  evals, evectors = sort_evals_descending(evals,evectors)
  score = change_of_basis(X,evectors)
  return score, evectors, evals

with plt.xkcd():
  score, evectors, evals = pca(X)
  plot_data_new_basis(score)
```

# Plot and explore the eigenvalues.

```python
#@title Video: Properties of PCA
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="uPcrB8B9S7A", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video

```

    Video available at https://youtube.com/watch?v=uPcrB8B9S7A

Finally, we will examine the eigenvalues of the covariance matrix, which represent the variance of the projected data. Run the code below to plot the eigenvalues (sometimes called the "scree plot"). Which eigenvalue is larger?

```python
 plot_eigenvalues(evals)
```

Now, run the following cell and use the slider to change the correlation coefficient in the data. You should see the scree plot and the plot of basis vectors update.

**Questions:**
* What happens to the eigenvalues as you change the correlation coefficient?
* Can you find a value when both eigenvalues are equal?
* Can you find a value when only one eigenvalue is nonzero?

```python
###### MAKE SURE TO RUN THIS CELL VIA THE PLAY BUTTON TO ENABLE SLIDERS ########

import ipywidgets as widgets

def refresh(corr_coef = .8):
  cov_matrix = calculate_cov_matrix(variance_1,variance_2,corr_coef)
  X = get_data(cov_matrix)
  score, evectors, evals = pca(X)
  plot_eigenvalues(evals)
  plot_basis_vectors(X,evectors)

_ = widgets.interact(refresh,
  corr_coef = (-1, 1, .1))

```