## Developing A Theoretical Understanding on the Limitations of ML interpretation methods

### Linear Regression

Let's start with a simple 10-variable regression problem. 

\begin{equation}
y = 2.0x_1 + 1.5x_2 + 1.2x_3 + 0.5x_4 + 0.2x_5 + x_6 + x_7 + x_8 + x_9 + x_{10} + \epsilon 
\end{equation}

where $x_i = \mathcal{U}(-1,1)$ and $\epsilon=\mathcal{N}(\mu=0, \sigma=0.1)$. In the special case where $\epsilon = 0$ (or at least sufficiently small), the predictor ranking is based on the magnitude of the coefficients.  


```python
import sys, os 
current_dir = os.getcwd()
path = os.path.dirname(current_dir)
sys.path.append(path)
import mintpy
import numpy as np 
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.model_selection import train_test_split
```


```python
n_examples = 2000
n_vars = 10 
weights = [2.0, 1.5, 1.2, 0.5, 0.2] + [1.0]*5
X = np.stack([np.random.uniform(-1,1, size=n_examples) for _ in range(n_vars)], axis=-1)
feature_names = [f'X_{i+1}' for i in range(n_vars)]
X = pd.DataFrame(X, columns=feature_names)

error = np.random.normal(loc=0, scale=0.1, size=n_examples)
y = X.dot(weights)#+error
```


```python
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0, test_size=0.25)

lr = LinearRegression()
lr.fit(X_train, y_train)
print('Learned Coefficients: ', lr.coef_)
```

    Learned Coefficients:  [2.  1.5 1.2 0.5 0.2 1.  1.  1.  1.  1. ]


The coefficients approximate the true coefficients quite well given the error. 


```python
myInterpreter = mintpy.InterpretToolkit(models=lr, model_names='Linear Regression', examples=X,targets=y,)
```


```python
results = myInterpreter.calc_ale(features=feature_names, n_bins=5)
```

    100%|██████████| 10/10 [00:00<00:00, 131.40it/s]



```python
results = myInterpreter.calc_permutation_importance(
                                               n_vars=len(feature_names), 
                                               evaluation_fn='mse', 
                                               n_bootstrap=1000, 
                                               subsample=1.0,
                                               n_jobs=10,
                                              )
```


```python
myInterpreter.plot_importance(method='singlepass')
```

Since the features are completely independent, the single-pass permutation method is good option for ranking the features. The results show the correct rankings: 
* $X_1$, $X_2$, and $X_3$ are the most important 
* $X_6-X_{10}$ have equivalent ranking (discerned by the CIs)
* $X_4-X_5$ are the least important


```python
myInterpreter.plot_importance(method='multipass')
```

# Increasing error

The following test includes increasing the error and seeing if the rankings are altered. 


```python
scales = np.arange(0.1, 1.7, 0.2)

ranked_features=[]
for s in scales:
    error = np.random.normal(loc=0, scale=s, size=n_examples)
    y = X.dot(weights)+error
    X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0, test_size=0.25)

    lr = LinearRegression()
    lr.fit(X_train, y_train)
    results = myInterpreter.calc_permutation_importance(
                                               n_vars=len(feature_names), 
                                               evaluation_fn='mse', 
                                               n_bootstrap=100, 
                                               subsample=1.0,
                                               n_jobs=10,
                                              )
    ranked_features.append(results['multipass_rankings__Linear Regression'].values)
```


```python
def get_avg_ranking(ranked_features, feature_names):
    """Compute the average rankings for a set of features"""
    
    rankings ={f:[] for f in feature_names}
    for rank in ranked_features:
        for f in feature_names:
            rankings[f].append(np.where(rank==f))
    
    for f in feature_names:
        rankings[f] = np.mean(rankings[f])
    
    return rankings

rankings = get_avg_ranking(ranked_features, feature_names)
```


```python
fig, ax = plt.subplots(dpi=150)
avg_rankings = [rankings[f] for f in feature_names]
ax.plot(np.arange(n_vars), avg_rankings, color='xkcd:medium blue', linewidth=2.5)
ax.set_xticks(np.arange(n_vars))
ax.set_xticklabels([f'${f}$' for f in feature_names])
ax.tick_params(labelsize=10)
ax.set_xlabel('Features')
ax.set_ylabel('Ranking (lower is better!)')
ax.grid(alpha=0.6)
```

## Introducing colinearity 

Takeaways
*
*


```python
fig, ax = plt.subplots(dpi=150)

scales = np.arange(0.1, 1.0, 0.1)
for s in scales:
    data1 = [np.random.uniform(-1,1, size=n_examples) for _ in range(int(n_vars/2))]
    data2 = [d+np.random.normal(0,s, size=n_examples) for d in data1]

    X = np.stack(data1+data2, axis=-1)
    feature_names = [f'X_{i+1}' for i in range(n_vars)]
    X = pd.DataFrame(X, columns=feature_names)

    print(X.corr().abs()['X_1']['X_6'])
    
    error = np.random.normal(loc=0, scale=0.1, size=n_examples)
    y = X.dot(weights)+error
    X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0, test_size=0.25)

    
    
    lr = LinearRegression()
    lr.fit(X_train, y_train)
    
    myInterpreter = mintpy.InterpretToolkit(models=lr, 
                                            model_names='Linear Regression', 
                                            examples=X_train,targets=y_train,
                                           )
    
    results = myInterpreter.calc_permutation_importance(
                                               n_vars=len(feature_names), 
                                               evaluation_fn='mse', 
                                               n_bootstrap=100, 
                                               subsample=1.0,
                                               n_jobs=10,
                                              )
    
    rankings = results['multipass_rankings__Linear Regression'].values
    ranked_features.append(rankings)
    rankings = [np.where(rankings==f)[0] for f in feature_names]
    
    ax.plot(np.arange(n_vars), rankings, linewidth=2.5, label=f'$\sigma$={s:.1f}')

ax.set_xticks(np.arange(n_vars))
ax.set_xticklabels([f'${f}$' for f in feature_names])
ax.tick_params(labelsize=10)
ax.set_xlabel('Features')
ax.set_ylabel('Ranking (lower is better!)')
ax.grid(alpha=0.6)
ax.legend()
ax.set_title('10-var Linear Regression\nWith Varying levels of Correlations')
    
```


```python
fig, ax = plt.subplots(dpi=150)

scales = np.arange(0.1, 1.7, 0.2)
for s in scales:
    data1 = [np.random.uniform(-1,1, size=n_examples) for _ in range(int(n_vars/2))]
    data2 = [d+np.random.normal(0,s, size=n_examples) for d in data1]

    X = np.stack(data1+data2, axis=-1)
    feature_names = [f'X_{i+1}' for i in range(n_vars)]
    X = pd.DataFrame(X, columns=feature_names)

    error = np.random.normal(loc=0, scale=0.5, size=n_examples)
    y = X.dot(weights)+error
    X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0, test_size=0.25)

    rf = RandomForestRegressor()
    rf.fit(X_train, y_train)
    
    myInterpreter = mintpy.InterpretToolkit(
                                            models=rf, 
                                            model_names='Random Forest', 
                                            examples=X_train,targets=y_train,
                                           )
    results = myInterpreter.calc_permutation_importance(
                                               n_vars=len(feature_names), 
                                               evaluation_fn='mse', 
                                               n_bootstrap=100, 
                                               subsample=1.0,
                                               n_jobs=10,
                                              )
    
    rankings = results['multipass_rankings__Random Forest'].values
    ranked_features.append(rankings)
    rankings = [np.where(rankings==f)[0] for f in feature_names]
    
    ax.plot(np.arange(n_vars), rankings, linewidth=2.5, label=f'$\sigma$={s:.1f}')

ax.set_xticks(np.arange(n_vars))
ax.set_xticklabels([f'${f}$' for f in feature_names])
ax.tick_params(labelsize=10)
ax.set_xlabel('Features')
ax.set_ylabel('Ranking (lower is better!)')
ax.grid(alpha=0.6)
ax.legend()
ax.set_title('10-var Random Forest\nWith Varying levels of Correlations')
   
```


```python

```

0a78d9a42f767701614834232a178e352557e2cc
ipynb
Jupyter Notebook
tutorial_notebooks/.ipynb_checkpoints/theoretical_interpretations-checkpoint.ipynb
monte-flora/scikit-explain
d93ca4c77d1d47e613479ae36cc055ffaafea88c
MIT
2021-04-12T16:11:38.000Z
2022-03-18T09:03:58.000Z
tutorial_notebooks/.ipynb_checkpoints/theoretical_interpretations-checkpoint.ipynb
monte-flora/py-mint
0dd22a2e3dce68d1a12a6a99623cb4d4cb407b58
MIT
2021-04-13T01:17:40.000Z
2022-03-11T16:06:50.000Z
tutorial_notebooks/.ipynb_checkpoints/theoretical_interpretations-checkpoint.ipynb
monte-flora/mintpy
23f9a952726dc0e69dfcdda2f8c7c27858aa9a11
MIT
2021-11-15T20:56:46.000Z
2021-11-15T20:56:46.000Z
Qwen/Qwen-72B
1. YES
2. YES