# Comparing Data
You'll often want to compare data in your dataset, to see if you can discern trends or relationships.

## Univariate Data
*Univariate* data is data that consist of only one variable or feature. While it may initially seem as though there's not much we can do to analyze univariate data, we've already seen that we can explore its distribution in terms of measures of central tendency and measures of variance. We've also seen how we can visualize this distribution using histograms and box plots.

Here's a reminder of how you can visualize the distribution of univariate data, using our student grade data with a few additional observations in the sample:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

plt.figure()
df['Grade'].plot( kind='box', title='Grade Distribution')
plt.figure()
df['Grade'].hist(bins=9)
plt.show()
print(df.describe())
print('median: ' + str(df['Grade'].median()))
```

## Bivariate and Multivariate Data
It can often be useful to compare *bivariate* data; in other words, compare two variables, or even more (in which case we call it *multivariate* data).

For example, our student data includes three numeric variables for each student: their salary, the number of hours they work per week, and their final school grade. Run the following code to see an enlarged sample of this data as a table:

```python
import pandas as pd

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df[['Name', 'Salary', 'Hours', 'Grade']]
```

      Name
      Salary
      Hours
      Grade

      0
      Dan
      50000
      41
      50

      1
      Joann
      54000
      40
      50

      2
      Pedro
      50000
      36
      46

      3
      Rosie
      189000
      17
      95

      4
      Ethan
      55000
      35
      50

      5
      Vicky
      40000
      39
      5

      6
      Frederic
      59000
      40
      57

      7
      Jimmie
      42000
      45
      42

      8
      Rhonda
      47000
      41
      26

      9
      Giovanni
      78000
      35
      72

      10
      Francesca
      119000
      30
      78

      11
      Rajab
      95000
      33
      60

      12
      Naiyana
      49000
      38
      40

      13
      Kian
      29000
      47
      17

      14
      Jenny
      130000
      24
      85

Let's suppose you want to compare the distributions of these variables. You might simply create a boxplot for each variable, like this:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df.plot(kind='box', title='Distribution', figsize = (10,8))
plt.show()
```

Hmm, that's not particularly useful is it?

The problem is that the data are all measured in different scales. Salaries are typically in tens of thousands, while hours and grades are in single or double digits.

### Normalizing Data
When you need to compare data in different units of measurement, you can *normalize* or *scale* the data so that the values are measured in the same proportional scale. For example, in Python you can use a MinMax scaler to normalize multiple numeric variables to a proportional value between 0 and 1 based on their minimum and maximum values. Run the following cell to do this:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.preprocessing import MinMaxScaler

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Normalize the data
scaler = MinMaxScaler()
df[['Salary', 'Hours', 'Grade']] = scaler.fit_transform(df[['Salary', 'Hours', 'Grade']])

# Plot the normalized data
df.plot(kind='box', title='Distribution', figsize = (10,8))
plt.show()
```

Now the numbers on the y axis aren't particularly meaningful, but they're on a similar scale.

### Comparing Bivariate Data with a Scatter Plot
When you need to compare two numeric values, a scatter plot can be a great way to see if there is any apparent relationship between them so that changes in the value of one variable affect the value of the other.

Let's look at a scatter plot of *Salary* and *Grade*:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Hours', x='Grade', y='Salary')
plt.show()

```

Look closely at the scatter plot. Can you see a diagonal trend in the plotted points, rising up to the right? It looks as though the higher the student's grade is, the higher their salary is.

You can see the trend more clearly by adding a *line of best fit* (sometimes called a *trendline*) to the plot:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Salary', x='Grade', y='Salary')

# Add a line of best fit
plt.plot(np.unique(df['Grade']), np.poly1d(np.polyfit(df['Grade'], df['Salary'], 1))(np.unique(df['Grade'])))

plt.show()
```

The line of best fit makes it clearer that there is some apparent *colinearity* between these variables (the relationship is *colinear* if one variable's value increases or decreases in line with the other).

### Correlation
The apparently colinear relationship you saw in the scatter plot can be verified by calculating a statistic that quantifies the relationship between the two variables. The statistic usually used to do this is *correlation*, though there is also a statistic named *covariance* that is sometimes used. Correlation is generally preferred because the value it produces is more easily interpreted.

A correlation value is always a number between ***-1*** and ***1***.
- A positive value indicates a positive correlation (as the value of variable *x* increases, so does the value of variable *y*).
- A negative value indicates a negative correlation (as the value of variable *x* increases, the value of variable *y* decreases).
- The closer to zero the correlation value is, the weaker the correlation between *x* and *y*.
- A correlation of exactly zero means there is no apparent relationship between the variables.

The formula to calculate correlation is:

\begin{equation}r_{x,y} = \frac{\displaystyle\sum_{i=1}^{n} (x_{i} -\bar{x})(y_{i} -\bar{y})}{\sqrt{\displaystyle\sum_{i=1}^{n} (x_{i} -\bar{x})^{2}(y_{i} -\bar{y})^{2}}}\end{equation}

**rx, y** is the notation for the *correlation between x and y*.

The formula is pretty complex, but fortunately Python makes it very easy to calculate the correlation by using the ***corr*** function:

```python
import pandas as pd

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000],
                   'Hours':[41,40,36,17,35,39,40],
                   'Grade':[50,50,46,95,50,5,57]})

# Calculate the correlation between *Salary* and *Grade*
print(df['Grade'].corr(df['Salary']))
```

    0.8149286388911882

In this case, the correlation is just over 0.8; making it a reasonably high positive correlation that indicates salary increases in line with grade.

Let's see if we can find a correlation between *Grade* and *Hours*:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

r = df['Grade'].corr(df['Hours'])
print('Correlation: ' + str(r))

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Hours', x='Grade', y='Hours')

# Add a line of best fit-
plt.plot(np.unique(df['Grade']), np.poly1d(np.polyfit(df['Grade'], df['Hours'], 1))(np.unique(df['Grade'])))
plt.show()

```

In this case, the correlation value is just under -0.8; meaning a fairly strong negative correlation in which the number of hours worked decreases as the grade increases. The line of best fit on the scatter plot corroborates this statistic.

It's important to remember that *correlation* **is not** *causation*. In other words, even though there's an apparent relationship, you can't say for sure that one variable is the cause of the other. In this example, we can say that students who achieved higher grades tend to work shorter hours; but we ***can't*** say that those who work shorter hours do so *because* they achieved a high grade!

## Least Squares Regression
In the previous examples, we drew a line on a scatter plot to show the *best fit* of the data. In many cases, your initial attempts to identify any colinearity might involve adding this kind of line by hand (or just mentally visualizing it); but as you may suspect from the use of the *numpy.**polyfit*** function in the code above, there are ways to calculate the coordinates for this line mathematically. One of the most commonly used techniques is *least squares regression*, and that's what we'll look at now.

Cast your mind back to when you were learning how to solve linear equations, and recall that the *slope-intercept* form of a linear equation lookes like this:

\begin{equation}y = mx + b\end{equation}

In this equation, *y* and *x* are the coordinate variables, *m* is the slope of the line, and *b* is the y-intercept of the line.

In the case of our scatter plot for our former-student's working hours, we already have our values for *x* (*Grade*) and *y* (*Hours*), so we just need to calculate the intercept and slope of the straight line that lies closest to those points. Then we can form a linear equation that calculates the a new *y* value on that line for each of our *x* (*Grade*) values - to avoid confusion, we'll call this new *y* value *f(x)* (because it's the output from a linear equation function based on *x*). The difference between the original *y* (*Hours*) value and the *f(x)* value is the *error* between our regression line of best fit and the actual *Hours* worked by the former student. Our goal is to calculate the slope and intercept for a line with the lowest overall error.

Specifically, we define the overall error by taking the error for each point, squaring it, and adding all the squared errors together. The line of best fit is the line that gives us the lowest value for the sum of the squared errors - hence the name *least squares regression*.

So how do we accomplish this? First we need to calculate the slope (*m*), which we do using this formula (in which *n* is the number of observations in our data sample):

\begin{equation}m = \frac{n(\sum{xy}) - (\sum{x})(\sum{y})}{n(\sum{x^{2}})-(\sum{x})^{2}}\end{equation}

After we've calculated the slope (*m*), we can use is to calculate the intercept (*b*) like this:

\begin{equation}b = \frac{\sum{y} - m(\sum{x})}{n}\end{equation}

Let's look at a simple example that compares the number of hours of nightly study each student undertook with the final grade the student achieved:

| Name     | Study | Grade |
|----------|-------|-------|
| Dan      | 1     | 50    |
| Joann    | 0.75  | 50    |
| Pedro    | 0.6   | 46    |
| Rosie    | 2     | 95    |
| Ethan    | 1     | 50    |
| Vicky    | 0.2   | 5     |
| Frederic | 1.2   | 57    |

First, let's take each *x* (Study) and *y* (Grade) pair and calculate *x2* and *xy*, because we're going to need these to work out the slope:

| Name     | Study | Grade | x2 | xy   |
|----------|-------|-------|------|------|
| Dan      | 1     | 50    | 1    | 50   |
| Joann    | 0.75  | 50    | 0.55 | 37.5 |
| Pedro    | 0.6   | 46    | 0.36 | 27.6 |
| Rosie    | 2     | 95    | 4    | 190  |
| Ethan    | 1     | 50    | 1    | 50   |
| Vicky    | 0.2   | 5     | 0.04 | 1    |
| Frederic | 1.2   | 57    | 1.44 | 68.4 |

Now we'll sum *x*, *y*, *x2*, and *xy*:

| Name     | Study | Grade | x2 | xy   |
|----------|-------|-------|------|------|
| Dan      | 1     | 50    | 1    | 50   |
| Joann    | 0.75  | 50    | 0.55 | 37.5 |
| Pedro    | 0.6   | 46    | 0.36 | 27.6 |
| Rosie    | 2     | 95    | 4    | 190  |
| Ethan    | 1     | 50    | 1    | 50   |
| Vicky    | 0.2   | 5     | 0.04 | 1    |
| Frederic | 1.2   | 57    | 1.44 | 68.4 |
| **Σ**      | **6.75**  | **353**   | **8.4025**| **424.5**  |

OK, now we're ready to calculate the slope for our *7* observations:

\begin{equation}m = \frac{(7\times 424.5) - (6.75\times353)}{(7\times8.4025)-6.75^{2}}\end{equation}

Which is:

\begin{equation}m = \frac{2971.5 - 2382.75}{58.8175-45.5625}\end{equation}

So:

\begin{equation}m = \frac{588.75}{13.255} \approx 44.4172\end{equation}

Now we can calculate *b*:

\begin{equation}b = \frac{353 - (44.4172\times6.75)}{7}\end{equation}

Which simplifies to:

\begin{equation}b = \frac{53.18389}{7} = 7.597699\end{equation}

Now we have our linear function:

\begin{equation}f(x) = 44.4172x + 7.597699\end{equation}

We can use this for each *x* (Study) value to calculate the *y* values for the regression line (*f(x)*), and we can subtract the original *y* (Grade) from these to calculate the error for each point:

| Name     | Study | Grade | *f(x)* | Error |
|----------|-------|-------|------|------ |
| Dan      | 1     | 50    |52.0149 |2.0149 |
| Joann    | 0.75  | 50    |40.9106 |-9.0894|
| Pedro    | 0.6   | 46    |34.2480 |-11.752|
| Rosie    | 2     | 95    |96.4321 |1.4321 |
| Ethan    | 1     | 50    |52.0149 |2.0149 |
| Vicky    | 0.2   | 5     |16.4811 |11.4811|
| Frederic | 1.2   | 57    |60.8983 |3.8983 |

As you can see, the *f(x)* values are mostly quite close to the actual *Grade* values, and the errors (which when we're comparing estimated values from a function with actual known values we we often call *residuals*) are generally pretty small.

Let's plot the least squares regression line with the actual values:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic'],
                   'Study':[1,0.75,0.6,2,1,0.2,1.2],
                   'Grade':[50,50,46,95,50,5,57],
                   'fx':[52.0159,40.9106,34.2480,96.4321,52.0149,16.4811,60.8983]})

# Create a scatter plot of Study vs Grade
df.plot(kind='scatter', title='Study Time vs Grade Regression', x='Study', y='Grade', color='red')

# Plot the regression line
plt.plot(df['Study'],df['fx'])

plt.show()
```

In this case, the line fits the middle values fairly well, but is less accurate for the outlier at the low end. This is often the case, which is why statisticians and data scientists often *treat* outliers by removing them or applying a threshold value; though in this example there are too few data points to conclude that the data points are really outliers.

Let's look at a slightly larger dataset and apply the same approach to compare *Grade* and *Salary*:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Calculate least squares regression line
df['x2'] = df['Grade']**2
df['xy'] = df['Grade'] * df['Salary']
x = df['Grade'].sum()
y = df['Salary'].sum()
x2 = df['x2'].sum()
xy = df['xy'].sum()
n = df['Grade'].count()
m = ((n*xy) - (x*y))/((n*x2)-(x**2))
b = (y - (m*x))/n
df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]
```

In this case, we used Python expressions to calculate the *slope* and *y-intercept* using the same approach and formula as before. In practice, Python provides great support for statistical operations like this; and you can use the ***linregress*** function in the *scipy.stats* package to retrieve the *slope* and *y-intercept* (as well as the *correlation*, *p-value*, and *standard error*) for a matched array of *x* and *y* values (we'll discuss *p-values* later!).

Here's the Python code to calculate the regression line variables using the ***linregress*** function:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from scipy import stats

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Get the regression line slope and intercept
m, b, r, p, se = stats.linregress(df['Grade'], df['Salary'])

df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]

```

Note that the *slope* and *y-intercept* values are the same as when we worked them out using the formula.

Similarly to the simple study hours example, the regression line doesn't fit the outliers very well. In this case, the extremes include a student who scored only 5, and a student who scored 95. Let's see what happens if we remove these students from our sample:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from scipy import stats

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df = df[(df['Grade'] > 5) & (df['Grade'] < 95)]

# Get the regression line slope and intercept
m, b, r, p, se = stats.linregress(df['Grade'], df['Salary'])

df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]

```

With the outliers removed, the line is a slightly better overall fit to the data.

One of the neat things about regression is that it gives us a formula and some constant values that we can use to estimate a *y* value for any *x* value. We just need to apply the linear function using the *slope* and *y-intercept* values we've calculated from our sample data. For example, suppose a student named Fabio graduates from our school with a final grade of **62**. We can use our linear function with the *slope* and *y-intercept* values we calculated with Python to estimate what salary he can expect to earn:

\begin{equation}f(x) = (1424.50\times62) - 7822.24 \approx 80,497 \end{equation}


## Contributions of visual and proprioceptive information to travelled distance estimation during changing sensory congruencies
Jennifer L Campos, John S Butler, Heinrich H. Bülthoff
Experimental Brain Research

__Abstract__ Recent research has provided evidence that visual and body-based cues (vestibular, proprioceptive
and efference copy) are integrated using a weighted linear sum during walking and passive transport. However, little
is known about the specific weighting of visual information when combined with proprioceptive inputs alone, in
the absence of vestibular information about forward selfmotion.

Therefore, in this study, participants walked in place on a stationary treadmill while dynamic visual information was updated in real time via a head-mounted display. The task required participants to travel a predefined distance and subsequently match this distance by adjusting an egocentric, in-depth target using a game controller. Travelled distance information was provided either through
visual cues alone, proprioceptive cues alone or both cues combined. In the combined cue condition, the relationship
between the two cues was manipulated by either changing the visual gain across trials (0.7×, 1.0×, 1.4×; Exp. 1) or
the proprioceptive gain across trials (0.7×, 1.0×, 1.4×; Exp. 2).

Results demonstrated an overall higher weighting of proprioception over vision. These weights were scaled, however, as a function of which sensory input provided more stable information across trials. Specifically, when visual gain was constantly manipulated, proprioceptive weights were higher than when proprioceptive gain was constantly manipulated. These results therefore reveal interesting characteristics of cue-weighting within the context of unfolding spatio-temporal cue dynamics.
# Introduction
In this notebook we will simulate the results of the Leaky Integrator for one condition from the paper by Campos et al. 2014 [1].

```python
## Library
import numpy as np # vector operations
import matplotlib.pyplot as plt # plotting data
```

## Leaky Integrator
The presented distance travelled for the visual, proprioceptive and congruent combined conditions was each fitted by a leaky spatial integrator mode.
The model describes the change of presented distance travelled ($p$) with respect to the change of the adjusted target distance estimate ($x$)
\begin{equation}
\frac{d p}{dx}=-\alpha p+k,
\end{equation}
for $0\leq x \leq 10m$, where $k$ and $\alpha$ are the sensory gain and the leak rate, respectively.
This model has the exact solution
\begin{equation}
p(x)=\big( \frac{k}{\alpha}\big)(1-exp(-\alpha x))
\end{equation}
In this simulation we set the leak rate as $\alpha=0.05$ and sensory gain as $k=0.9$.

```python
alpha=0.05
k=0.9
```

## Discerete Domain
To numerically approximate the solution we discretise the continuous domain using a step size, $h=1$,
which gives
\begin{equation}
x_i=0+i h,
\end{equation}
for $i=0,...10$.

The Figure below illustrate the discrete domain.

```python
h=1
x_end=10
# CREATES A LIST OF NUMBER BETWEEN 0 AND 10 (x_end)
x=np.arange(0,x_end+h/2,h)

## THIS MAKES PLOT
fig = plt.figure(1,figsize=(5,4))
ax = fig.add_subplot(1,1,1)
ax.plot(x,0*x,'o:',color='k')
ax.set_xlabel('Distance Presented (m)')
plt.show()
```

```python
N=int(x_end/h) # Number of Steps

P=np.zeros(N+1) # Numerical Solution of the differential equation

p=k/alpha*(1-np.exp(-alpha*x))
# Initial Conditions
P[0]=0
```

We numerically approximate the differential equation using Eulers method to give the difference equation
\begin{equation}
P_{i+1}=P_{i}+h \big(-\alpha P_{i}+k \big),
\end{equation}
where $h$ is the stepsize and where $P_i$ is the numerical approximation of $p(x)$ at distance $x_i$, for $i=0,...,10$

```python
for i in range (0,N):
    ## THIS IS THE DIFFERENCE EQUATION
    P[i+1]=P[i]+h*(-alpha*P[i]+k)

```

## Results
The plot below shows the Numerical solution $P_i$ (left plot in red), the analytic solution $p(x)$ (middle plot in green) and the error $p(x_i)-P_i$ (right in red) as a function of presented distance $x$. The dashed black line in the left and middle plot represents perfect recall of the distance.

```python
## Plotting Figure
fig = plt.figure(1,figsize=(12,4))

# --- left hand plot
ax = fig.add_subplot(1,3,1)
ax.plot(x,P,'o:',color='blue')
ax.plot(x,x,'--k')

ax.set_xlabel('Distance Presented (m)')
ax.set_ylabel('Response Distance (m)')
#ax.legend(loc='best')
ax.set_title('Numerical Solution')

# --- middle plot
ax = fig.add_subplot(1,3,2)
ax.plot(x,p,'-',color='green')
ax.plot(x,x,'--k')

ax.set_xlabel('Distance Presented (m)')
ax.set_ylabel('Response Distance (m)')
ax.set_title('Analytic (exact) Solution')

# --- right hand plot
ax = fig.add_subplot(1,3,3)
ax.plot(x,p-P,'o:',color='red')
ax.set_title('Error')
ax.set_ylabel('Error(m)')
ax.set_xlabel('Distance Presented (m)')

# --- title, explanatory text and save
fig.suptitle('Leaky Solution', fontsize=20)
plt.tight_layout()
plt.subplots_adjust(top=0.85)

```

# References

[1] Campos, J. L., Butler, J. S., & Bülthoff, H. H. (2014). Contributions of visual and proprioceptive information to travelled distance estimation during changing sensory congruencies. Experimental brain research, 232(10), 3277-3289.


```python
import numpy as np
import sympy as sym
import pydae.build as db
from pydae.grid_bpu import bpu
```

```python
bpu_obj = bpu(data_input='oc_3bus_uvsg_high.json')

g_list = bpu_obj.dae['g']
h_dict = bpu_obj.dae['h_dict']
f_list = bpu_obj.dae['f']
x_list = bpu_obj.dae['x']
params_dict = bpu_obj.dae['params_dict']

sys = {'name':'oc_3bus_uvsg_high',
       'params_dict':params_dict,
       'f_list':f_list,
       'g_list':g_list,
       'x_list':x_list,
       'y_ini_list':bpu_obj.dae['y_ini'],
       'y_run_list':bpu_obj.dae['y_run'],
       'u_run_dict':bpu_obj.dae['u_run_dict'],
       'u_ini_dict':bpu_obj.dae['u_ini_dict'],
       'h_dict':h_dict}

sys = db.system(sys)
db.sys2num(sys)
```

```python
H = 5.0; # desired virtual inertia
K_p = 0.01; # active power proportinal gain
#H = T_p/K_p/2

T_p = K_p*2*H;  # active power integral time constant
```

```python
T_p
```

    0.1

```python
160**2*0.2e-6
```

    0.0051199999999999996

```python

```


# Trying to understand what metrics where used

```python
! pip install sympy
```

    Collecting sympy
      Using cached https://files.pythonhosted.org/packages/dd/f6/ed485ff22efdd7b371d0dbbf6d77ad61c3b3b7e0815a83c89cbb38ce35de/sympy-1.3.tar.gz
    Collecting mpmath>=0.19 (from sympy)
      Downloading https://files.pythonhosted.org/packages/ca/63/3384ebb3b51af9610086b23ea976e6d27d6d97bf140a76a365bd77a3eb32/mpmath-1.1.0.tar.gz (512kB)
        100% |████████████████████████████████| 522kB 5.3MB/s ta 0:00:011
    Building wheels for collected packages: sympy, mpmath
      Running setup.py bdist_wheel for sympy ... done
      Stored in directory: /home/group7/.cache/pip/wheels/6c/59/86/478e3c0f298368c119095cc5985dedac57c0e35a85c737f823
      Running setup.py bdist_wheel for mpmath ... done
      Stored in directory: /home/group7/.cache/pip/wheels/63/9d/8e/37c3f6506ed3f152733a699e92d8e0c9f5e5f01dea262f80ad
    Successfully built sympy mpmath

```python
from pathlib import Path
import pandas as pd
import sklearn.metrics
from sympy import symbols, nonlinsolve
```

```python
data ="""Persuasive   0.81 0.84 0.91
Audience   0.80 0.99 0.88
Agreement  0.69 0.85 0.76
Informative   0.76 0.74 0.75
Mean   0.74 0.78 0.75
Controversial   0.67 0.64 0.65
Disagreement  0.60 0.68 0.64
topic 0.62 0.67 0.61
Sentiment  0.44 0.46 0.43"""
```

```python
arr = data.split('\n')
```

```python
arr
```

    ['Persuasive   0.81 0.84 0.91',
     'Audience   0.80 0.99 0.88',
     'Agreement  0.69 0.85 0.76',
     'Informative   0.76 0.74 0.75',
     'Mean   0.74 0.78 0.75',
     'Controversial   0.67 0.64 0.65',
     'Disagreement  0.60 0.68 0.64',
     'topic 0.62 0.67 0.61',
     'Sentiment  0.44 0.46 0.43']

```python
pr_val, re_val, f1_val = {}, {}, {}
```

```python
for l in arr:
    name, prec, reca, f1sc = l.split()
    pr_val[name] = float(prec)
    re_val[name] = float(reca)
    f1_val[name] = float(f1sc)
```

```python
def f1(p, r):
    return 2 * p * r * 1 / (p + r)

def prec(true_pos, false_pos):
    return true_pos / (false_pos + true_pos)

def reca(true_pos, false_neg):
    return true_pos  / (true_pos + false_neg)
```

```python
# Persuasive
f1(0.81, 0.84) # 0.91 🛑
```

    0.8247272727272728

```python
# Audience
f1(0.80, 0.99) # 0.88 ✔
```

    0.8849162011173185

```python
# Agreement w/ com
f1(0.69, 0.85) # 0.76 ✔
```

    0.7616883116883115

```python
# Informative
f1(0.76, 0.74) # 0.75 ✔
```

    0.7498666666666667

```python
# Mean
f1(0.74, 0.78) # 0.75 ✔
```

    0.7594736842105264

```python
# controversial
f1(0.67, 0.64) # 0.65 ✔
```

    0.6546564885496183

```python
# disagree
f1(0.60, 0.68) # 0.64 ✔
```

    0.6375000000000001

```python
# off-topic
f1(0.62, 0.67) # 0.61 🛑
```

    0.644031007751938

```python
# sentiment
f1(0.44, 0.46)  # 0.43 🛑
```

    0.4497777777777778

```python
pth = Path('/mnt/data/group07/johannes/ynacc_proc/replicate/split/val.csv')
df = pd.read_csv(pth)
```

```python
def solve(name):
    counts = df['cl' + name.lower()].value_counts()
#     the reverse to ensure that there is only posibile solution

#         for i in reversed([[0, 1], [1, 0]]):
    all_res = []
    for i in [[0, 1], [1, 0]]:
        count_0 = counts[i[0]]
        count_1 = counts[i[1]]

        tp, tn, fp, fn = symbols('tp, tn, fp, fn', real=True)
        eq1 = count_1 - tp - fn
        eq2 = count_0 - tn - fp
        eq3 = prec(tp, fp) - pr_val[name]
        eq4 = reca(tp, fn) - re_val[name]

        system = [eq1, eq2, eq3, eq4]
        res = nonlinsolve(system, [tp, tn, fp, fn])
        print(res)
#         if len(res) > 0 and all(x > 0 for x in list(res)[0]):
        all_res.append(list(list(res)[0]) + [i])
    return all_res
```

```python
def kappa(name):
    solutions = solve(name)
    print(solutions)
    for s in solutions:
        tp, tn, fp, fn, i = s
        y_true, y_pred = [], []

        for x in range(round(tp)):
            y_true.append(1)
            y_pred.append(1)

        for x in range(round(tn)):
            y_true.append(0)
            y_pred.append(0)

        for x in range(round(fp)):
            y_true.append(0)
            y_pred.append(1)

        for x in range(round(fn)):
            y_true.append(1)
            y_pred.append(0)
        print(i)
        print('kappa:  ', sklearn.metrics.cohen_kappa_score(y_true, y_pred))
        print('f1_mic: ', sklearn.metrics.f1_score(y_true, y_pred, average='micro'))
        print('f1_mac: ',sklearn.metrics.f1_score(y_true, y_pred, average='macro'))
```

```python
for k in f1_val.keys():
    print(k)
    kappa(k)
    print()
```

    Persuasive
    {(71.4, 481.251851851852, 16.7481481481481, 13.6)}
    {(418.32, -13.1244444444444, 98.1244444444444, 79.68)}
    [[71.4000000000000, 481.251851851852, 16.7481481481481, 13.6000000000000, [0, 1]], [418.320000000000, -13.1244444444444, 98.1244444444444, 79.6800000000000, [1, 0]]]
    [0, 1]
    kappa:   0.7896017415802279
    f1_mic:  0.9468267581475128
    f1_mac:  0.8947953594234788
    [1, 0]
    kappa:   -0.17343597911689246
    f1_mic:  0.7013422818791947
    f1_mac:  0.41222879684418146

    Audience
    {(378.18, 104.455, 94.545, 3.82)}
    {(197.01, 332.7475, 49.2525, 1.99)}
    [[378.180000000000, 104.455000000000, 94.5450000000000, 3.82000000000000, [0, 1]], [197.010000000000, 332.747500000000, 49.2525000000000, 1.99000000000000, [1, 0]]]
    [0, 1]
    kappa:   0.5751386806319849
    f1_mic:  0.8296041308089501
    f1_mac:  0.7808674781416081
    [1, 0]
    kappa:   0.8155406288713061
    f1_mic:  0.9122203098106713
    f1_mac:  0.9071317756569979

    Agreement
    {(57.8, 489.031884057971, 25.968115942029, 10.2)}
    {(437.75, -128.670289855072, 196.670289855072, 77.25)}
    [[57.8000000000000, 489.031884057971, 25.9681159420290, 10.2000000000000, [0, 1]], [437.750000000000, -128.670289855072, 196.670289855072, 77.2500000000000, [1, 0]]]
    [0, 1]
    kappa:   0.7281065395377759
    f1_mic:  0.9382504288164666
    f1_mac:  0.8638274680784803
    [1, 0]
    kappa:   -0.1841456752655537
    f1_mic:  0.6151685393258427
    f1_mac:  0.38086956521739135

    Informative
    {(71.04, 464.566315789474, 22.4336842105263, 24.96)}
    {(360.38, -17.8042105263158, 113.804210526316, 126.62)}
    [[71.0400000000000, 464.566315789474, 22.4336842105263, 24.9600000000000, [0, 1]], [360.380000000000, -17.8042105263158, 113.804210526316, 126.620000000000, [1, 0]]]
    [0, 1]
    kappa:   0.7032307675645233
    f1_mic:  0.9193825042881647
    f1_mac:  0.8516081515057974
    [1, 0]
    kappa:   -0.249868404021228
    f1_mic:  0.5990016638935108
    f1_mac:  0.3746097814776275

    Mean
    {(94.38, 428.839459459459, 33.1605405405405, 26.62)}
    {(360.36, -5.61297297297298, 126.612972972973, 101.64)}
    [[94.3800000000000, 428.839459459459, 33.1605405405405, 26.6200000000000, [0, 1]], [360.360000000000, -5.61297297297298, 126.612972972973, 101.640000000000, [1, 0]]]
    [0, 1]
    kappa:   0.6927536231884058
    f1_mic:  0.8970840480274442
    f1_mac:  0.8463525195024246
    [1, 0]
    kappa:   -0.23774696484450275
    f1_mic:  0.6112054329371817
    f1_mac:  0.37934668071654365

    Controversial
    {(120.32, 335.737910447761, 59.2620895522388, 67.68)}
    {(252.8, 63.4865671641791, 124.513432835821, 142.2)}
    [[120.320000000000, 335.737910447761, 59.2620895522388, 67.6800000000000, [0, 1]], [252.800000000000, 63.4865671641791, 124.513432835821, 142.200000000000, [1, 0]]]
    [0, 1]
    kappa:   0.4951417252500734
    f1_mic:  0.7821612349914236
    f1_mac:  0.7475011339105763
    [1, 0]
    kappa:   -0.023822834930511405
    f1_mic:  0.5420240137221269
    f1_mac:  0.487601591894374

    Disagreement
    {(159.8, 241.466666666667, 106.533333333333, 75.2)}
    {(236.64, 77.24, 157.76, 111.36)}
    [[159.800000000000, 241.466666666667, 106.533333333333, 75.2000000000000, [0, 1]], [236.640000000000, 77.2400000000000, 157.760000000000, 111.360000000000, [1, 0]]]
    [0, 1]
    kappa:   0.36530363210030137
    f1_mic:  0.6878216123499142
    f1_mac:  0.6816769068305093
    [1, 0]
    kappa:   0.008985838773072685
    f1_mic:  0.5385934819897084
    f1_mac:  0.5010102167113708

    topic
    {(268.0, 18.741935483871, 164.258064516129, 132.0)}
    {(122.61, 324.851935483871, 75.148064516129, 60.39)}
    [[268.000000000000, 18.7419354838710, 164.258064516129, 132.000000000000, [0, 1]], [122.610000000000, 324.851935483871, 75.1480645161290, 60.3900000000000, [1, 0]]]
    [0, 1]
    kappa:   -0.23743689765947673
    f1_mic:  0.4922813036020583
    f1_mac:  0.3790016121602948
    [1, 0]
    kappa:   0.47409040793825796
    f1_mic:  0.7684391080617495
    f1_mac:  0.7368473845227945

    Sentiment
    {(76.82, 209.229090909091, 97.7709090909091, 90.18)}
    {(141.22, -12.7345454545454, 179.734545454545, 165.78)}
    [[76.8200000000000, 209.229090909091, 97.7709090909091, 90.1800000000000, [0, 1]], [141.220000000000, -12.7345454545454, 179.734545454545, 165.780000000000, [1, 0]]]
    [0, 1]
    kappa:   0.14032684404483975
    f1_mic:  0.6033755274261603
    f1_mac:  0.5700306872792542
    [1, 0]
    kappa:   -0.5495576686101047
    f1_mic:  0.28952772073921973
    f1_mac:  0.22452229299363058

```python
kappa('topic')
```

    {(268.0, 18.741935483871, 164.258064516129, 132.0)}
    kappa:   -0.23743689765947673
    f1_mic:  0.4922813036020583
    f1_mac:  0.3790016121602948

```python
from sympy.solvers.solveset import nonlinsolve
from sympy.core.symbol import symbols
```

```python
# binary
tp, tn, fp, fn = symbols('tp, tn, fp, fn', real=True)
eq1 = 188 - tp - fn
eq2 = 395 - tn - fp
eq3 = prec(tp, fp) - 0.67
eq4 = reca(tp, fn) - 0.64
eq5 = f1(eq3, eq4) - 0.65

system = [eq1, eq2, eq3, eq4]
nonlinsolve(system, [tp, tn, fp, fn])
```

    {(120.32, 215.417910447761, 179.582089552239, 67.68)}

```python
# micro
a, b, c, d = symbols('a, b, c, d', real=True)
eq1 = 188 - a - b
eq2 = 395 - c - d
eq3 = prec(a + d, b + c) - 0.67
eq4 = reca(a + d, b + c) - 0.64
# eq5 = f1(eq3, eq4) - 0.65

system = [eq1, eq2, eq3, eq4]
nonlinsolve(system, [a, b, c, d])
```

    EmptySet()

```python
# macro
a, b, c, d = symbols('a, b, c, d', real=True)
eq1 = 188 - a - b
eq2 = 395 - c - d
eq3 = (prec(a, c) + prec(d, b)) / 2 - 0.67
eq4 = (reca(a, b) + reca(d, c)) / 2 - 0.64
eq5 = (f1(prec(a, c), reca(a, b)) + f1(prec(d, b), reca(d, c))) / 2 - 0.65

system = [eq1, eq2, eq3, eq4]
nonlinsolve(system, [a, b, c, d])
```

    {(-0.67*d - 170.65*sqrt(-1.89242129226345e-5*d**2 + 0.005711568519704*d + 1.0) + 358.65, 0.67*d + 170.65*sqrt(-1.89242129226345e-5*d**2 + 0.005711568519704*d + 1.0) - 170.65, -d + 395, d), (-0.67*d + 170.65*sqrt(-1.89242129226345e-5*d**2 + 0.005711568519704*d + 1.0) + 358.65, 0.67*d - 170.65*sqrt(-1.89242129226345e-5*d**2 + 0.005711568519704*d + 1.0) - 170.65, -d + 395, d)}

```python
# weighted
a, b, c, d = symbols('a, b, c, d', real=True)
eq1 = 188 - a - b
eq2 = 395 - c - d
eq3 = ((a + b) * prec(a, c) + (c + d) * prec(d, b)) / (a + b + c + d) - 0.67
eq4 = ((a + b) * reca(a, b) + (c + d) * reca(d, c)) / (a + b + c + d) - 0.64
eq5 = ((a + b) * f1(prec(a, c), reca(a, b)) + (c + d) * f1(prec(d, b), reca(d, c))) / (a + b + c + d) - 0.65

system = [eq1, eq2, eq3, eq4]
nonlinsolve(system, [a, b, c, d])
```

    {(-14.4493657422819, 202.449365742282, 7.43063425771811, 387.569365742282), (582.023911196827, -394.023911196827, 603.903911196827, -208.903911196827)}

```python

```

code/ynacc/09 Replicate Reported Baseline/Replicate Metrics.ipynb


```python
import pulp
import pandas
import numpy as np
```

```python
Calidad={"Primera","Segunda","Tercera"}
TiposAditivos={"A","B"}

Stock={"A":200,"B":1000}
MBase=1500
Req={"Primera":{"A":0.500,"B":0},
     "Segunda":{"A":0.250,"B":0.350},
     "Tercera":{"A":0,"B":0.800}}
Precio={"Primera":2000,"Segunda":1800,"Tercera":1500}

```

```python
prob = pulp.LpProblem("Fabrica_ladrillos", pulp.LpMaximize)

x = pulp.LpVariable.dicts("Produccion", (c for c in Calidad), lowBound=0, cat='Continuous')

```

```python
prob += pulp.lpSum([x[c]*Precio[c] for c in Calidad])

```

```python
for i in TiposAditivos:
    prob += 0<=pulp.lpSum([x[c]*Req[c][i] for c in Calidad])<= Stock[i]

prob += pulp.lpSum([x[c] for c in Calidad]) <= MBase

# for i in TiposAditivos:
#     prob += pulp.lpSum([x[c]*Req[c][i] for c in Calidad])== a[i]

# prob += pulp.lpSum([x[c] for c in Calidad]) <= MBase

# for i in TiposAditivos :
#     prob += a[i]<= Stock[i]

```

```python
# Choose solver, and set it to problem, and build the Gurobi model
solver = pulp.GUROBI()
prob.setSolver(solver)
prob.solver.buildSolverModel(prob)

# Solve model
prob.solve()

print("#################################################################")

print("\nVariables:\n")

for i in prob.variables():
    print(i, i.varValue)

```

    Using license file /home/maxi/anaconda3/gurobi.lic
    Academic license - for non-commercial use only
    Gurobi Optimizer version 9.0.1 build v9.0.1rc0 (linux64)
    Optimize a model with 3 rows, 3 columns and 7 nonzeros
    Model fingerprint: 0x915cddde
    Coefficient statistics:
      Matrix range     [2e-01, 1e+00]
      Objective range  [2e+03, 2e+03]
      Bounds range     [0e+00, 0e+00]
      RHS range        [2e+02, 2e+03]
    Presolve time: 0.02s
    Presolved: 3 rows, 3 columns, 7 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    5.3000000e+33   5.650000e+30   5.300000e+03      0s
           3    2.4900000e+06   0.000000e+00   0.000000e+00      0s

    Solved in 3 iterations and 0.02 seconds
    Optimal objective  2.490000000e+06
    Gurobi status= 2
    #################################################################

    Variables:

    Produccion_Primera 0.0
    Produccion_Segunda 800.0
    Produccion_Tercera 700.0

```python
prob
```

    Fabrica_ladrillos:
    MAXIMIZE
    2000*Produccion_Primera + 1800*Produccion_Segunda + 1500*Produccion_Tercera + 0
    SUBJECT TO
    _C1: 0.5 Produccion_Primera + 0.25 Produccion_Segunda <= 200

    _C2: 0.35 Produccion_Segunda + 0.8 Produccion_Tercera <= 1000

    _C3: Produccion_Primera + Produccion_Segunda + Produccion_Tercera <= 1500

    VARIABLES
    Produccion_Primera Continuous
    Produccion_Segunda Continuous
    Produccion_Tercera Continuous

```python
sol=[x["Primera"].varValue,x["Segunda"].varValue,x["Tercera"].varValue]
```

```python
import numpy as np
from sympy import Plane, Point3D
import networkx as nx

def simplify(triangles):
    """
    Simplify an iterable of triangles such that adjacent and coplanar triangles form a single face.
    Each triangle is a set of 3 points in 3D space.
    """

    # create a graph in which nodes represent triangles;
    # nodes are connected if the corresponding triangles are adjacent and coplanar
    G = nx.Graph()
    G.add_nodes_from(range(len(triangles)))
    for ii, a in enumerate(triangles):
        for jj, b in enumerate(triangles):
            if (ii < jj): # test relationships only in one way as adjacency and co-planarity are bijective
                if is_adjacent(a, b):
                    if is_coplanar(a, b, np.pi / 180.):
                        G.add_edge(ii,jj)

    # triangles that belong to a connected component can be combined
    components = list(nx.connected_components(G))
    simplified = [set(flatten(triangles[index] for index in component)) for component in components]

    # need to reorder nodes so that patches are plotted correctly
    reordered = [reorder(face) for face in simplified]

    return reordered

def is_adjacent(a, b):
    return len(set(a) & set(b)) == 2 # i.e. triangles share 2 points and hence a side

def is_coplanar(a, b, tolerance_in_radians=0):
    a1, a2, a3 = a
    b1, b2, b3 = b
    plane_a = Plane(Point3D(a1), Point3D(a2), Point3D(a3))
    plane_b = Plane(Point3D(b1), Point3D(b2), Point3D(b3))
    if not tolerance_in_radians: # only accept exact results
        return plane_a.is_coplanar(plane_b)
    else:
        angle = plane_a.angle_between(plane_b).evalf()
        angle %= np.pi # make sure that angle is between 0 and np.pi
        return (angle - tolerance_in_radians <= 0.) or \
            ((np.pi - angle) - tolerance_in_radians <= 0.)

flatten = lambda l: [item for sublist in l for item in sublist]

def reorder(vertices):
    """
    Reorder nodes such that the resulting path corresponds to the "hull" of the set of points.

    Note:
    -----
    Not tested on edge cases, and likely to break.
    Probably only works for convex shapes.

    """
    if len(vertices) <= 3: # just a triangle
        return vertices
    else:
        # take random vertex (here simply the first)
        reordered = [vertices.pop()]
        # get next closest vertex that is not yet reordered
        # repeat until only one vertex remains in original list
        vertices = list(vertices)
        while len(vertices) > 1:
            idx = np.argmin(get_distance(reordered[-1], vertices))
            v = vertices.pop(idx)
            reordered.append(v)
        # add remaining vertex to output
        reordered += vertices
        return reordered

def get_distance(v1, v2):
    v2 = np.array(list(v2))
    difference = v2 - v1
    ssd = np.sum(difference**2, axis=1)
    return np.sqrt(ssd)
```

```python
%matplotlib widget

from scipy.spatial import HalfspaceIntersection
from scipy.spatial import ConvexHull
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as a3
import matplotlib.colors as colors

w = np.array([1., 1., 1.])

# ∑ᵢ hᵢ wᵢ qᵢ - ∑ᵢ gᵢ wᵢ <= 0
#  qᵢ - ubᵢ <= 0
# -qᵢ + lbᵢ <= 0
halfspaces = np.array([
                    [1.*w[0], 1.*w[1], 1.*w[2], -1500 ],
                    [ 0,  0.350,  0.800, -1000],
                    [ 0.500,  0.250,  0., -200],
                    [-1.,  0.,  0.,  0],
                    [ 0., -1.,  0.,  0],
                    [ 0.,  0., -1.,  0]
                    ])
feasible_point = np.array([0.1, 0.1, 0.11])
hs = HalfspaceIntersection(halfspaces, feasible_point)
verts = hs.intersections
verts = np.around(verts)
hull = ConvexHull(verts)
faces = hull.simplices

fig=plt.figure()
fig.canvas.layout.width = '7in'

ax = a3.Axes3D(fig)
ax.dist=10
ax.azim=30
ax.elev=10

lim_sup=np.max(verts)
lim_inf=np.min(verts)
ax.set_xlim([lim_inf,lim_sup])
ax.set_ylim([lim_inf,lim_sup])
ax.set_zlim([lim_inf,lim_sup])

triangles = []
for s in faces:
    sq = [
        (verts[s[0], 0], verts[s[0], 1], verts[s[0], 2]),
        (verts[s[1], 0], verts[s[1], 1], verts[s[1], 2]),
        (verts[s[2], 0], verts[s[2], 1], verts[s[2], 2])
    ]
    triangles.append(sq)

new_faces = simplify(triangles)
for sq in new_faces:
    f = a3.art3d.Poly3DCollection([sq])
    f.set_color(colors.rgb2hex(sp.rand(3)))
    f.set_edgecolor('k')
    f.set_alpha(0.1)
    ax.add_collection3d(f)

ax.scatter(sol[0],sol[1],sol[2],s=100)
ax.set_xlabel('Primera')
ax.set_ylabel('Segunda')
ax.set_zlabel('Tercera')

# # rotate the axes and update
# for angle in range(0, 360):
#     ax.view_init(30, angle)
#     plt.draw()
#     plt.pause(.001)
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    /home/maxi/anaconda3/envs/opti/lib/python3.6/site-packages/ipykernel_launcher.py:61: DeprecationWarning: scipy.rand is deprecated and will be removed in SciPy 2.0.0, use numpy.random.rand instead

    Text(0.5, 0, 'Tercera')

```python
s

```

    array([5, 7, 6], dtype=int32)

```python

```


### Introduction to simplicial homology

```python
# uncomment to install the panel library
#pip install panel

import numpy as np
import matplotlib.pyplot as plt
from bokeh import palettes
%config InlineBackend.figure_format="retina"

import panel as pn
pn.extension()
```

### Affinely independent points

We say that the set of vectors $\{\vec{a}_0, \vec{a}_1, .., \vec{a}_n \}$ is affinely independent, if $\{ \vec{a}_1 - \vec{a}_0, .., \vec{a}_n - \vec{a}_0 \}$ is linearly independent.

### n-simplices

An n-simplex $\sigma$ spanned by $\{\vec{a}_0, \vec{a}_1, .., \vec{a}_n \}$ to be the. set of all points $\vec{x} \in \mathbb{R}^n$ such that

$$
\sigma = \left \{ \sum_{i = 0}^n t_i \vec{a}_i \, | \, \sum_{i=0}^n t_i = 1, t_i \ge 0 \forall i \right \}
$$

That is, the coefficients $t_i$ form a probability distribution, and are uniquely determined. This form is called the barycentric coordinates of a point $x$.

#### Examples of n-simplices

First, let's begin in $\mathbb{R}^2$

1. A 1-simplex is defined as

$$
\vec{x} = t \vec{a}_0 + (1-t) \vec{a}_1
$$

Let $a_0 = (1, 0), a_1 = (1,1)$, then

```python
t_slider = pn.widgets.FloatSlider(
    name = r"scalar t", start = 0.0, end = 1, step = 0.01, value = 0.5
)
```

```python
# uncomment to select color palette
# palettes.Blues3
```

```python
@pn.depends(t_slider.param.value)
def one_simplex(t):
    "An app to visualize a 1-simplex."
    a_0 = np.array([[0, 1]]).T
    a_1 = np.array([[1, 1]]).T

    x = t * a_0 + (1 - t) * a_1

    fig = plt.figure(figsize=(6, 5))
    plt.plot(
        [a_0[0], a_1[0]],
        [a_0[1], a_1[1]],
        color="lightgrey",
        label=r"$\sigma$ (1-simplex)",
    )
    plt.scatter(*a_0, label="a_0", color="#deebf7")
    plt.scatter(*a_1, label="a_1", color="#3182bd")
    plt.scatter(*x, label="x", color="#9ecae1")

    # plot scaled a_0
    len_a0 = np.sqrt(np.linalg.norm(a_0))
    plt.arrow(
        x=0,
        y=0,
        dx=t * a_0[0][0],
        dy=t * a_0[1][0],
        color="#deebf7",
        label="a_0 scaled",
    )
    plt.arrow(
        x=0,
        y=0,
        dx=(1 - t) * a_1[0][0],
        dy=(1 - t) * a_1[1][0],
        color="#3182bd",
        label="a_1 scaled",
    )

    plt.arrow(
        x=0,
        y=0,
        dx=x[0][0],
        dy=x[1][0],
        color="#9ecae1",
        label="x vector",
    )

    plt.legend(loc="lower right")
    plt.tight_layout()
    # plt.axis("off")
    plt.close(fig)
    return fig
```

```python
pn.Column(
    "### 1-simplex",
    t_slider,
    one_simplex
)
```

2. The $2$-simplex can be specified as follows:

\begin{align}
\vec{x} &= t_0 \vec{a}_0 + (1- t_0) \vec{p}\\[1.em]
&= t_0 \vec{a}_0 + (1- t_0) \left[\left( \frac{t_1}{\lambda} \right) \vec{a}_1 + \left( \frac{t_2}{\lambda} \right) \vec{a}_2 ) \right]
\end{align}

where $\lambda = 1- \vec{t}_0$.

```python
t1_slider = pn.widgets.FloatSlider(
    name = r"scalar t1", start = 0.0, end = 1, step = 0.01, value = 0.5
)

t2_slider = pn.widgets.FloatSlider(
    name = r"scalar t2", start = 0.0, end = 1, step = 0.01, value = 0.5
)
```

```python
#uncomment to select color palette
#palettes.Set2
```

```python
@pn.depends(t1_slider.param.value, t2_slider.param.value)
def two_simplex(t_1, t_2):
    "An app to visualize a 1-simplex."
    a_1 = np.array([[0, 1]]).T
    a_2 = np.array([[1, 1]]).T
    a_3 = np.array([[1, 0]]).T

    t_3 = 1 - (t_1 + t_2)
    x = t_1 * a_1 + t_2 * a_2 + t_3 * a_3

    lg="lightgrey"
    fig = plt.figure(figsize=(6, 5))
    # a1 - a2
    plt.plot([a_1[0], a_2[0]],[a_1[1], a_2[1]],color=lg)
    # a2 - a3
    plt.plot([a_2[0], a_3[0]],[a_2[1], a_3[1]],color=lg)
    # a1 - a3
    plt.plot(
        [a_1[0], a_3[0]],[a_1[1], a_3[1]],color=lg,
        label=r"$\sigma$ (1-simplex)"
    )

    set2=('#66c2a5', '#fc8d62', '#8da0cb')
    plt.scatter(*a_1, label="a_1", color=set2[0])
    plt.scatter(*a_2, label="a_2", color=set2[1])
    plt.scatter(*a_3, label="a_3", color=set2[2])
    plt.scatter(*x, color="#3182bd")

    # plot scaled a_1
    plt.arrow(
        x=0,y=0,
        dx=t_1 * a_1[0][0],
        dy=t_1 * a_1[1][0],
        color=set2[0],
        #label="a_0 scaled",
    )

    plt.arrow(
        x=0,
        y=0,
        dx= t_2 * a_2[0][0],
        dy= t_2 * a_2[1][0],
        color=set2[1],
        #label="a_1 scaled",
    )

    plt.arrow(
        x=0,
        y=0,
        dx= t_3 * a_3[0][0],
        dy= t_3 * a_3[1][0],
        color= set2[2] ,
        #label="a_1 scaled",
    )

    plt.arrow(
        x=0,
        y=0,
        dx=x[0][0],
        dy=x[1][0],
        color="#3182bd",
        label="x vector",
    )

    plt.legend(loc="lower right")
    plt.tight_layout()
    # plt.axis("off")
    plt.close(fig)
    return fig
```

```python
pn.Column(
    "### 2-simplex",
    t1_slider,
    t2_slider,
    two_simplex
)
```

```python

```

#### Convex sets, convex hull

```python

```

#### p-chains

```python

```

### Boundary operator

```python

```

### Computing the Betti numbers from a dataset

```python

```


# Section 2.3 $\quad$ Elementary Matrices; Finding $A^{-1}$

## Definition

An **$n\times n$ elementary matrix of type I, II, or III** is

### Example 1

The following are elementary matrices
\begin{equation*}
  E_1 = \left[
     \begin{array}{ccc}
       0 & 0 & 1  \\
       0 & 1 & 0  \\
       1 & 0 & 0  \\
     \end{array}
   \right],~~~
   E_2 = \left[
     \begin{array}{ccc}
       1 & 0 & 0  \\
       0 & -3 & 0  \\
       0 & 0 & 1  \\
     \end{array}
   \right],~~~
   E_3 = \left[
     \begin{array}{ccc}
       1 & 2 & 0  \\
       0 & 1 & 0  \\
       0 & 0 & 1  \\
     \end{array}
   \right].
\end{equation*}

>**Theorem** Let $A$ be an $m\times n$ matrix, and let an elementary row operation be performed on $A$ to yield matrix $B$, then

**Remark:** Let $A$ and $B$ be matrices.

1. $A$ is row equivalent to $B$ if and only if

- An elementary matrix $E$ is

- $A$ is nonsingular if and only if

- Let $A$ be an $n\times n$ matrix. The homogeneous system $A\mathbf{x}=\mathbf{0}$ has a nontrivial solution if and only if

## Finding $A^{-1}$

A nonsingular matrix $A$ is row equivalent to $I_n$, so that

\begin{equation*}
  A = \hspace{3in}
\end{equation*}

Hence

\begin{equation*}
  A^{-1} = \hspace{3in}
\end{equation*}

Performing elementary row operations to the matrix $[A~|~I_n]$, we obtain

### Example 2

Find $A^{-1}$ if
\begin{equation*}
  A =
  \left[
    \begin{array}{ccc}
      1 & 1 & 1 \\
      0 & 2 & 3 \\
      5 & 5 & 1 \\
    \end{array}
  \right]
\end{equation*}

```python
from sympy import *

A = Matrix([[1, 1, 1], [0, 2, 3], [5, 5, 1]]);

A.inv()
```

    Matrix([
    [ 13/8, -1/2, -1/8],
    [-15/8,  1/2,  3/8],
    [  5/4,    0, -1/4]])

>**Theorem** An $n\times n$ matrix $A$ is singular if and only if

**Remark**

### Example 3

Find $A^{-1}$ if
\begin{equation*}
  A =
  \left[
    \begin{array}{ccc}
      1 & 2 & -3 \\
      1 & -2 & 1 \\
      5 & -2 & -3 \\
    \end{array}
  \right]
\end{equation*}

```python
from sympy import *

A = Matrix([[1, 2, -3], [1, -2, 1], [5, -2, -3]]);

A.inv()
```

>**Theorem** If $A$ and $B$ are $n\times n$ matrices such that $AB = I_n$, then

**Remark**

### Exercise

Find all values of $a$ for which the inverse of
\begin{equation*}
  A =
  \left[
    \begin{array}{ccc}
      0 & 1 & 2 \\
      2 & 1 & 1 \\
      1 & a & 2 \\
    \end{array}
  \right]
\end{equation*}
exists. What is $A^{-1}$?

```python
from sympy import *

a = symbols('a');
A = Matrix([[0, 1, 2], [2, 1, 1], [1, a, 2]]);

A.det()
```

    4*a - 5

```python
from sympy import *

a = symbols('a');
A = Matrix([[0, 1, 2], [2, 1, 1], [1, a, 2]]);

A.inv()
```

    Matrix([
    [-1/2 + (-a + 1/2)/(2*(-2*a + 5/2)), 1/2 - 1/(4*(-2*a + 5/2)), 1/(2*(-2*a + 5/2))],
    [     1 - 2*(-a + 1/2)/(-2*a + 5/2),           1/(-2*a + 5/2),    -2/(-2*a + 5/2)],
    [           (-a + 1/2)/(-2*a + 5/2),      -1/(2*(-2*a + 5/2)),     1/(-2*a + 5/2)]])


# Test for causation: Convergent Cross Mapping

This method looks for the signature of time-series $x_1$ in time-series $x_2$ by seeing whether there is a correspondence between the library of points in the attractor manifold built from $x_2$, $M_2$, and points in the $x_1$ manifold, $M_1$, where these two manifolds are constructed from lagged coordinates of the time-series variables. For more details on that, please refer to the notebook "Nonlinear prediction of chaotic dynamical systems"

```python
import numpy as np
import matplotlib.pyplot as plt
# import dataset
import statsmodels.api as sm
```

Consider as an example the dataset included in statsmodel, German inflation rate vs. interest 1972-1998 (measured each quartile).
A proxy for inflation is a measure of GDP deflator (\url{https://en.wikipedia.org/wiki/GDP_deflator}).
Here (https://www.statsmodels.org/dev/datasets/generated/interest_inflation.html) it is reported the delta of the log GDP deflator (Dp) vs. the long term interest rate (r).

```python
data = sm.datasets.interest_inflation.load()
```

```python
# We are interested in Dp and rate (see documentation)
Dp = data['data']['Dp']
rate = data['data']['R']
```

```python
plt.plot(Dp,label='Dp')
plt.plot(rate,label='rate')
plt.legend()
plt.xlabel('Time from 1972 (quartiles)')
plt.show()
```

#### Is there causation? I.e., does the interest rate affect the GDP? Or is the opposite true?

In 2012 Sugihara et al. (https://www.science.org/doi/10.1126/science.1227079) proposed a method for testing causation in dynamical systems called Convergent Cross-Mapping (CCM).

Practically, the idea is to test whether nearby points on the $M_2$ manifold can be used to identify nearby points on $M_1$. If so, then one can use $x_2$ to estimate $x_1$ and vice versa!

In detail, CCM proceeds as follows:

**(1)** Given an embedding dimension $n$, compute lag-vectors $x_1^p(t) = [ x_1(t), x_1(t-1), \dots, x_1(t-n)]$ for $t = n, n+1, \dots, T$
**(2)** Find the $n+1$ nearest neighbors of each lag-vector $x_1^p(t^*)$, which will give a set of Euclidean distances
    \begin{equation}
    \label{CCM_dis}\tag{1}
        \{ d_1, \dots, d_{n+1}\}_{t^*}
    \end{equation}
    and associated times
    \begin{equation}\label{CCM_tims}\tag{2}
        \{\hat t_1, \dots, \hat t_{n+1}\}_{t^*}
    \end{equation}
**(3)** Find the corresponding weights of the neighbors using (\ref{CCM_dis}):
    \begin{equation}\label{CCM_weights}\tag{3}
    w_i = \frac{e^{-d_i}}{\sum_i^{n+1} e^{-d_i}}
    \end{equation}

**(4)** Predict $x_2(t^*)$ based on using the weights (\ref{CCM_weights}) and times (\ref{CCM_tims}), which were computed from $x_1$, and accordingly denote this by $\hat x_2(t^*)_{| x_1}$:
    \begin{equation}\tag{4}
    \hat x_2(t^*)_{| x_1} = \sum_i^{n+1} w_i x_2(\hat t_i)
    \end{equation}
**(5)** Compute the squared correlation between real $x_2$ and reconstructed time series $\hat x_2{}_{|x_1}$ as
    \begin{equation}\label{CCM_corr}\tag{5}
    C_{2|1} = \rho (x_2, \hat x_2{}_{| x_1})^2
    \end{equation}
Any pair of times series then has two CCM correlations, $C_{1|2}$ and $C_{2|1}$, which allow to determine CCM causality.  Afterwards, several scenarios are possible, as explained below. If $x_2$ can be estimated using $x_1$, but $x_1$ cannot be estimated using $x_2$, then $x_1$ is said to ``CCM cause'' $x_2$. In particular, Sugihara and colleagues define a difference of CCM correlations to determine the CCM causality between $x_1$ and $x_2$:
\begin{equation}
    \Delta_C = C_{1|2} - C_{2|1}.
\end{equation}
By studying the sign of $\Delta_C$, and the magnitudes of $C_{1|2}$ and $C_{2|1}$ it is possible to fix thresholds to determine the flow of information. It is possible that the causality flow is balanced ($\Delta_C \approx 0$ and $C_{1|2} \approx C_{2|1} \approx 1$), or unbalanced ($|\Delta_C| > 0$) or absent ($C_{1|2} \approx C_{2|1} \approx 0$).

The CCM algorithm depends on the embedding dimension $n$. The optimal embedding dimension and time lag are found by analysing the nonlinear prediction of the single time series, similarly to what we explained in the previous notebook.

```python
### these are the scripts explained in the previous notebook ###

from sklearn.neighbors import NearestNeighbors
from scipy import stats as stats

# split dataset into train/test
def train_test_split(X, fraction_train=.75):
    split = int(len(X)*fraction_train)
    return X[:split], X[split:]

# exponential weights: w_i = exp(-d_i) / sum_i exp(-d_i)
def weights(distances):
    num = np.exp(-distances) # numerator: e^{-d_i}
    den = np.sum(num,axis=1,keepdims=True) # denominator: sum_i e^{-d_i}
    return num/den

# embed vectors into n-dimensional past values (the last element is the one to be predicted)
def embed_vectors(X, n_embed):
    size = len(X)
    leng = size-n_embed
    out_vects = np.zeros((leng,n_embed + 1))
    for i in range(leng):
        out_vects[i,:] = X[i:i+n_embed+1]
    return out_vects

# implement the Sugihara nonlinear prediction
def nonlinear_prediction(X_train, X_test, n_embed):

    # initialize nearest neighbors from sklearn
    knn = NearestNeighbors(n_neighbors=n_embed+1)

    # Nearest neigbors is fit on the train data (only on the past vectors - i.e. till [:-1])
    knn.fit(X_train[:,:-1])

    # find the nearest neighbors for each test vector input
    dist,ind = knn.kneighbors(X_test[:,:-1])

    # compute exponential weights given distances
    W = weights(dist)

    # predict test using train (weighted average)
    x_pred = np.sum(X_train[ind][:,-1] * W, axis=1)

    return x_pred
```

```python
### this is the novel part ###

# this algorithm is very similar to the previous "nonlinear_prediction"
# the only tricky part is that weights for X1 are used to predict X2, and vice versa!

def ccm_predict(X1_train, X2_train, X1_test, X2_test,n_embed):

    # initialize nearest neighbors from sklearn
    knn1 = NearestNeighbors(n_neighbors=n_embed+1)
    knn2 = NearestNeighbors(n_neighbors=n_embed+1)
    knn1.fit(X1_train[:,:-1])
    knn2.fit(X2_train[:,:-1])

    # find the nearest neighbors for each test vector input
    dist1,ind1 = knn1.kneighbors(X1_test[:,:-1])
    dist2,ind2 = knn2.kneighbors(X2_test[:,:-1])

    # compute exponential weights given distances
    W1 = weights(dist1)
    W2 = weights(dist2)

    # predict x1 using ind2, W2, and opposite predicte x2 using ind1, W1
    x1_p = np.sum(X1_train[ind2][:,-1] * W2, axis=1)
    x2_p = np.sum(X2_train[ind1][:,-1] * W1, axis=1)

    return x1_p, x2_p
```

Now it's time to apply this method to the data.

First, let's find the best embedding dimension.

```python
# find best embedding by using nonlinear prediction of each time series
nonlinear_reconstr_cor = []
nonlinear_reconstr_ssr = []
for n_embed in np.arange(1,20):

    # embed points in n_embed-dimensional space
    X1 = embed_vectors(Dp,n_embed)
    X2 = embed_vectors(rate,n_embed)

    # split train/test
    X1_train, X1_test = train_test_split(X1, fraction_train=0.7)
    X2_train, X2_test = train_test_split(X2, fraction_train=0.7)
    # nonlinear prediction on individual time series
    x1_p = nonlinear_prediction(X1_train, X1_test, n_embed)
    x2_p = nonlinear_prediction(X2_train, X2_test, n_embed)

    # simply check correlation
    nonlinear_reconstr_cor.append([np.corrcoef(X1_test[:,-1], x1_p)[0,1],
                                 np.corrcoef(X2_test[:,-1], x2_p)[0,1]])

    # measure sum squared residuals
    nonlinear_reconstr_ssr.append([np.sum((X1_test[:,-1] - x1_p)**2),
                                   np.sum((X2_test[:,-1] - x2_p)**2)])
```

```python
plt.plot(np.arange(1,20),np.sum(nonlinear_reconstr_cor,1))
plt.xticks(np.arange(1,20))
plt.xlabel('Embedding dimension (n_embed)')
plt.ylabel('Pearson correlation (sum)')
plt.axvline(x=4,color='k',ls='dotted')
plt.show()
```

```python
plt.plot(np.arange(1,20),np.sum(nonlinear_reconstr_ssr,1))
plt.xticks(np.arange(1,20))
plt.axvline(x=4,color='k',ls='dotted')
plt.ylabel('SSR')
plt.show()
```

Now, let's predict "each other" using CCM

```python
n_embed = 4 # best one according to previous plot

X1 = embed_vectors(Dp,n_embed)
X2 = embed_vectors(rate,n_embed)
```

```python
# split train/test
X1_train, X1_test = train_test_split(X1, fraction_train=0.7)
X2_train, X2_test = train_test_split(X2, fraction_train=0.7)

# prediction
x1_pred, x2_pred = ccm_predict(X1_train,X2_train,X1_test, X2_test,n_embed)
```

Now check whether the prediction of X1 (based on X2) is good:

```python
stats.pearsonr(x1_pred,X1_test[:,-1])
```

    (0.3482430948233711, 0.05487831860154601)

```python
stats.pearsonr(x2_pred,X2_test[:,-1])
```

    (0.28489860459616817, 0.12031416240759091)

Now, we see that X1 (i.e. DP, which is a proxy for inflation) can be predicted better than X2 (i.e. interest rate).

We conclude that rate CCM-causes inflation.

## Comparison with Granger Causality

```python
from statsmodels.tsa.stattools import grangercausalitytests
```

Let's begin to test whether interest rate Granger-causes DP (i.e. inflation)

```python
gra_res = grangercausalitytests(np.array([Dp,rate]).T, maxlag=5,verbose=False)
gra_res[1]
```

    ({'ssr_ftest': (6.8945947764847855, 0.009962976033159838, 103.0, 1),
      'ssr_chi2test': (7.095408216576575, 0.007728168786045521, 1),
      'lrtest': (6.868025019467723, 0.008775157719255171, 1),
      'params_ftest': (6.894594776484797, 0.00996297603315977, 103.0, 1.0)},
     [<statsmodels.regression.linear_model.RegressionResultsWrapper at 0x7f2a952cf210>,
      <statsmodels.regression.linear_model.RegressionResultsWrapper at 0x7f2a9528f0d0>,
      array([[0., 1., 0.]])])

Yes, the test is significant. We can reject the null hypothesis of interest rate NOT causing DP

Now test the opposite: does infaltion cause interest rate?

```python
gra_res = grangercausalitytests(np.array([rate,Dp]).T, maxlag=5,verbose=False)
gra_res[1]
```

    ({'ssr_ftest': (0.031230236128532308, 0.8600747368583583, 103.0, 1),
      'ssr_chi2test': (0.03213985465654781, 0.8577210554160729, 1),
      'lrtest': (0.03213498313994023, 0.8577317235928832, 1),
      'params_ftest': (0.03123023612851477, 0.8600747368584091, 103.0, 1.0)},
     [<statsmodels.regression.linear_model.RegressionResultsWrapper at 0x7f2a952d03d0>,
      <statsmodels.regression.linear_model.RegressionResultsWrapper at 0x7f2a952d02d0>,
      array([[0., 1., 0.]])])

The test is not significant. We cannot reject the null hypothesis of DP NOT causing interest rate

Notebook 2 Convergent Cross Mapping.ipynb


# Non Linear Regression Analysis

If the data shows a curvy trend, then linear regression will not produce very accurate results when compared to a non-linear regression because, as the name implies, linear regression presumes that the data is linear.
Let's learn about non linear regressions and apply an example on python. In this notebook, we fit a non-linear model to the datapoints corrensponding to China's GDP from 1960 to 2014.

### Importing required libraries

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

Though Linear regression is very good to solve many problems, it cannot be used for all datasets. First recall how linear regression, could model a dataset. It models a linear relation between a dependent variable y and independent variable x. It had a simple equation, of degree 1, for example y = 2*(x) + 3.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 2*(x) + 3
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
#plt.figure(figsize=(8,6))
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

Non-linear regressions are a relationship between independent variables $x$ and a dependent variable $y$ which result in a non-linear function modeled data. Essentially any relationship that is not linear can be termed as non-linear, and is usually represented by the polynomial of $k$ degrees (maximum power of $x$).

$$ \ y = a x^3 + b x^2 + c x + d \ $$

Non-linear functions can have elements like exponentials, logarithms, fractions, and others. For example: $$ y = \log(x)$$

Or even, more complicated such as :
$$ y = \log(a x^3 + b x^2 + c x + d)$$

Let's take a look at a cubic function's graph.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 1*(x**3) + 1*(x**2) + 1*x + 3
y_noise = 20 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

As you can see, this function has $x^3$ and $x^2$ as independent variables. Also, the graphic of this function is not a straight line over the 2D plane. So this is a non-linear function.

Some other types of non-linear functions are:

### Quadratic

$$ Y = X^2 $$

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

y = np.power(x,2)
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Exponential

An exponential function with base c is defined by $$ Y = a + b c^X$$ where b ≠0, c > 0 , c ≠1, and x is any real number. The base, c, is constant and the exponent, x, is a variable.

```python
X = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

Y= np.exp(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Logarithmic

The response $y$ is a results of applying logarithmic map from input $x$'s to output variable $y$. It is one of the simplest form of __log()__: i.e. $$ y = \log(x)$$

Please consider that instead of $x$, we can use $X$, which can be polynomial representation of the $x$'s. In general form it would be written as
\begin{equation}
y = \log(X)
\end{equation}

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = np.log(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Sigmoidal/Logistic

$$ Y = a + \frac{b}{1+ c^{(X-d)}}$$

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = 1-4/(1+np.power(3, X-2))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

# Non-Linear Regression example

For an example, we're going to try and fit a non-linear model to the datapoints corrensponding to China's GDP from 1960 to 2014. We download a dataset with two columns, the first, a year between 1960 and 2014, the second, China's corresponding annual gross domestic income in US dollars for that year.

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

#downloading dataset
!wget -nv -O china_gdp.csv https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/china_gdp.csv

df = pd.read_csv("china_gdp.csv")
df.head(10)
```

    2020-05-27 10:50:58 URL:https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/china_gdp.csv [1218/1218] -> "china_gdp.csv" [1]

      Year
      Value

      0
      1960
      5.918412e+10

      1
      1961
      4.955705e+10

      2
      1962
      4.668518e+10

      3
      1963
      5.009730e+10

      4
      1964
      5.906225e+10

      5
      1965
      6.970915e+10

      6
      1966
      7.587943e+10

      7
      1967
      7.205703e+10

      8
      1968
      6.999350e+10

      9
      1969
      7.871882e+10

__Did you know?__ When it comes to Machine Learning, you will likely be working with large datasets. As a business, where can you host your data? IBM is offering a unique opportunity for businesses, with 10 Tb of IBM Cloud Object Storage: [Sign up now for free](http://cocl.us/ML0101EN-IBM-Offer-CC)

### Plotting the Dataset ###
This is what the datapoints look like. It kind of looks like an either logistic or exponential function. The growth starts off slow, then from 2005 on forward, the growth is very significant. And finally, it deaccelerates slightly in the 2010s.

```python
plt.figure(figsize=(8,5))
x_data, y_data = (df["Year"].values, df["Value"].values)
plt.plot(x_data, y_data, 'ro')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```

### Choosing a model ###

From an initial look at the plot, we determine that the logistic function could be a good approximation,
since it has the property of starting with a slow growth, increasing growth in the middle, and then decreasing again at the end; as illustrated below:

```python
X = np.arange(-5.0, 5.0, 0.1)
Y = 1.0 / (1.0 + np.exp(-X))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

The formula for the logistic function is the following:

$$ \hat{Y} = \frac1{1+e^{\beta_1(X-\beta_2)}}$$

$\beta_1$: Controls the curve's steepness,

$\beta_2$: Slides the curve on the x-axis.

### Building The Model ###
Now, let's build our regression model and initialize its parameters.

```python
def sigmoid(x, Beta_1, Beta_2):
     y = 1 / (1 + np.exp(-Beta_1*(x-Beta_2)))
     return y
```

Lets look at a sample sigmoid line that might fit with the data:

```python
beta_1 = 0.10
beta_2 = 1990.0

#logistic function
Y_pred = sigmoid(x_data, beta_1 , beta_2)

#plot initial prediction against datapoints
plt.plot(x_data, Y_pred*15000000000000.)
plt.plot(x_data, y_data, 'ro')
```

Our task here is to find the best parameters for our model. Lets first normalize our x and y:

```python
# Lets normalize our data
xdata =x_data/max(x_data)
ydata =y_data/max(y_data)
```

#### How we find the best parameters for our fit line?
we can use __curve_fit__ which uses non-linear least squares to fit our sigmoid function, to data. Optimal values for the parameters so that the sum of the squared residuals of sigmoid(xdata, *popt) - ydata is minimized.

popt are our optimized parameters.

```python
from scipy.optimize import curve_fit
popt, pcov = curve_fit(sigmoid, xdata, ydata)
#print the final parameters
print(" beta_1 = %f, beta_2 = %f" % (popt[0], popt[1]))
```

     beta_1 = 690.451713, beta_2 = 0.997207

Now we plot our resulting regresssion model.

```python
x = np.linspace(1960, 2015, 55)
x = x/max(x)
plt.figure(figsize=(8,5))
y = sigmoid(x, *popt)
plt.plot(xdata, ydata, 'ro', label='data')
plt.plot(x,y, linewidth=3.0, label='fit')
plt.legend(loc='best')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```

## Practice
Can you calculate what is the accuracy of our model?

```python
# write your code here

msk = np.random.rand(len(df)) < 0.8
train_x = xdata[msk]
test_x = xdata[~msk]
train_y = ydata[msk]
test_y = ydata[~msk]

popt, pcov = curve_fit(sigmoid, train_x, train_y)
y_hat = sigmoid(test_x, *popt)
# evaluation
print("Mean absolute error: %.2f" % np.mean(np.absolute(y_hat - test_y)))
print("Residual sum of squares (MSE): %.2f" % np.mean((y_hat - test_y) ** 2))
from sklearn.metrics import r2_score
print("R2-score: %.2f" % r2_score(y_hat , test_y) )
```

    Mean absolute error: 0.03
    Residual sum of squares (MSE): 0.00
    R2-score: 0.88

Double-click __here__ for the solution.

## Want to learn more?

IBM SPSS Modeler is a comprehensive analytics platform that has many machine learning algorithms. It has been designed to bring predictive intelligence to decisions made by individuals, by groups, by systems – by your enterprise as a whole. A free trial is available through this course, available here: [SPSS Modeler](http://cocl.us/ML0101EN-SPSSModeler).

Also, you can use Watson Studio to run these notebooks faster with bigger datasets. Watson Studio is IBM's leading cloud solution for data scientists, built by data scientists. With Jupyter notebooks, RStudio, Apache Spark and popular libraries pre-packaged in the cloud, Watson Studio enables data scientists to collaborate on their projects without having to install anything. Join the fast-growing community of Watson Studio users today with a free account at [Watson Studio](https://cocl.us/ML0101EN_DSX)

### Thanks for completing this lesson!

Notebook created by: Saeed Aghabozorgi

Copyright © 2018 [Cognitive Class](https://cocl.us/DX0108EN_CC). This notebook and its source code are released under the terms of the [MIT License](https://bigdatauniversity.com/mit-license/).


# Matrix Factorization for Recommendations in Python

In this post, I'll detail a basic version of low-rank matrix factorization for recommendations employ it on a dataset of 1 million movie ratings (from 1 to 5) available from the [MovieLens](http://grouplens.org/datasets/movielens/) project. The MovieLens datasets were created collected by GroupLens Research at the University of Minnesota.

[Previously](https://beckernick.github.io/music_recommender/), I used item-based collaborative filtering to make music recommendations from raw artist listen-count data. I had a relatively small amount of data, and ended up making some pretty good recommendations. Collaborative filtering methods that compute distance relationships between items or users are generally thought of as "neighborhood" methods, since they center on the idea of "nearness". Unfortunately, there are two issues with taking this approach:

1. It doesn't scale particularly well to massive datasets
2. There's a theoretical concern with raw data based approaches.

I talked about the scaling issue in the previous post, but not the conceptual issue. The key concern is that ratings matrices may be overfit and noisy representations of user tastes and preferences. When we use distance based "neighborhood" approaches on raw data, we match to sparse low-level details that we assume represent the user's preference vector instead of the vector itself. It's a subtle difference, but it's important.

If I've listened to ten Red Hot Chili Peppers songs and you've listened to ten different Red Hot Chili Peppers songs, the raw user action matrix wouldn't have any overlap. We'd have nothing in common, even though it seems pretty likely we share at least some underlying preferencs.

If it sounds like using song features (such as genre) could help, you're right. But, to steal Joseph Konstan's (professor at Minnesota involved with GroupLens Research who has an awesome [Coursera course](https://www.coursera.org/specializations/recommender-systems) on Recommender Systems) example, what if we both like songs with great storytelling, regardless of the genre. So, how do we resolve this? I would need a method that can derive the tastes and preference vectors from the raw data.

Low-Rank Matrix Factorization is that kind of method.

# Matrix Factorization via Singular Value Decomposition

Matrix factorization is the breaking down of one matrix in a product of multiple matrices. It's extremely well studied in mathematics, and it's highly useful. There are many different ways to factor matrices, but singular value decomposition is particularly useful for making recommendations.

So what is singular value decomposition (SVD)? At a high level, SVD is an algorithm that decomposes a matrix $R$ into the best lower rank (i.e. smaller/simpler) approximation of the original matrix $R$. Mathematically, it decomposes R into a two unitary matrices and a diagonal matrix:

$$\begin{equation}
R = U\Sigma V^{T}
\end{equation}$$

where R is users's ratings matrix, $U$ is the user "features" matrix, $\Sigma$ is the diagonal matrix of singular values (essentially weights), and $V^{T}$ is the movie "features" matrix. $U$ and $V^{T}$ are orthogonal, and represent different things. $U$ represents how much users "like" each feature and $V^{T}$ represents how relevant each feature is to each movie.

To get the lower rank approximation, we take these matrices and keep only the top $k$ features, which we think of as the underlying tastes and preferences vectors.

# Setting Up the Ratings Data

Okay, enough with the math. Let's get to the code.

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

ratings_list = [i.strip().split("::") for i in open('./Research Paper/ml-1m/ratings.dat', 'r').readlines()]
users_list = [i.strip().split("::") for i in open('./Research Paper//ml-1m/users.dat', 'r').readlines()]
movies_list = [i.strip().split("::") for i in open('./Research Paper//ml-1m/movies.dat', 'r').readlines()]
```

```python
ratings = np.array(ratings_list)
users = np.array(users_list)
movies = np.array(movies_list)
```

```python
ratings_df = pd.DataFrame(ratings_list, columns = ['UserID', 'MovieID', 'Rating', 'Timestamp'], dtype = int)
movies_df = pd.DataFrame(movies_list, columns = ['MovieID', 'Title', 'Genres'])
movies_df['MovieID'] = movies_df['MovieID'].apply(pd.to_numeric)
```

I'll also take a look at the movies and ratings dataframes.

```python
movies_df.head()
```

      MovieID
      Title
      Genres

      0
      1
      Toy Story (1995)
      Animation|Children's|Comedy

      1
      2
      Jumanji (1995)
      Adventure|Children's|Fantasy

      2
      3
      Grumpier Old Men (1995)
      Comedy|Romance

      3
      4
      Waiting to Exhale (1995)
      Comedy|Drama

      4
      5
      Father of the Bride Part II (1995)
      Comedy

```python
ratings_df.head()
```

      UserID
      MovieID
      Rating
      Timestamp

      0
      1
      1193
      5
      978300760

      1
      1
      661
      3
      978302109

      2
      1
      914
      3
      978301968

      3
      1
      3408
      4
      978300275

      4
      1
      2355
      5
      978824291

These look good, but I want the format of my ratings matrix to be one row per user and one column per movie. I'll `pivot` `ratings_df` to get that and call the new variable `R`.

```python
R_df = ratings_df.pivot(index = 'UserID', columns ='MovieID', values = 'Rating').fillna(0)
R_df.head()
```

      MovieID
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      ...
      3943
      3944
      3945
      3946
      3947
      3948
      3949
      3950
      3951
      3952

      UserID

      1
      5.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      2
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      3
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      4
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      5
      0.0
      0.0
      0.0
      0.0
      0.0
      2.0
      0.0
      0.0
      0.0
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

5 rows × 3706 columns

The last thing I need to do is de-mean the data (normalize by each users mean) and convert it from a dataframe to a numpy array.

```python
R = R_df.as_matrix()
user_ratings_mean = np.mean(R, axis = 1)
R_demeaned = R - user_ratings_mean.reshape(-1, 1)
```

All set. With my ratings matrix properly formatted and normalized, I'm ready to do the singular value decomposition

# Singular Value Decomposition

Scipy and Numpy both have functions to do the singular value decomposition. I'm going to use the Scipy function `svds` because it let's me choose how many latent factors I want to use to approximate the original ratings matrix (instead of having to truncate it after).

```python
from scipy.sparse.linalg import svds
U, sigma, Vt = svds(R_demeaned, k = 100)
```

Done. The function returns exactly what I detailed earlier in this post, except that the $\Sigma$ returned is just the values instead of a diagonal matrix. This is useful, but since I'm going to leverage matrix multiplication to get predictions I'll convert it to the diagonal matrix form.

```python
sigma = np.diag(sigma)
```

# Making Predictions from the Decomposed Matrices

I now have everything I need to make movie ratings predictions for every user. I can do it all at once by following the math and matrix multiply $U$, $\Sigma$, and $V^{T}$ back to get the rank $k=50$ approximation of $R$.

I also need to add the user means back to get the actual star ratings prediction.

```python
all_user_predicted_ratings = np.dot(np.dot(U, sigma), Vt) + user_ratings_mean.reshape(-1, 1)
```

If I wanted to put this kind of system into production, I'd want to create a training and validation set and optimize the number of latent features ($k$) by minimizing the Root Mean Square Error. Intuitively, the Root Mean Square Error will decrease on the training set as $k$ increases (because I'm approximating the original ratings matrix with a higher rank matrix).

However, for movies, between around 20 and 100 feature "preferences" vectors have been found to be optimal for generalizing to unseen data.

I could create a training and validation set and optimize $k$ by minimizing RMSE, but since I'm just going through proof of concept I'll leave that for another post. I just want to see some movie recommendations.

# Making Movie Recommendations
Finally, it's time. With the predictions matrix for every user, I can build a function to recommend movies for any user. All I need to do is return the movies with the highest predicted rating that the specified user hasn't already rated. Though I didn't use actually use any explicit movie content features (such as genre or title), I'll merge in that information to get a more complete picture of the recommendations.

I'll also return the list of movies the user has already rated, for the sake of comparison.

```python
preds_df = pd.DataFrame(all_user_predicted_ratings, columns = R_df.columns)
preds_df.head()
```

      MovieID
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      ...
      3943
      3944
      3945
      3946
      3947
      3948
      3949
      3950
      3951
      3952

      0
      5.157608
      0.184833
      0.348341
      -0.022609
      0.139622
      -0.156937
      -0.061122
      0.072117
      0.018278
      -0.372566
      ...
      -0.111771
      -0.002460
      0.016625
      -0.107081
      -0.051609
      0.022706
      -0.114028
      0.009476
      0.070798
      -0.195959

      1
      0.557186
      0.296927
      0.078853
      -0.013888
      0.028675
      1.092160
      -0.054492
      0.114191
      0.090106
      1.695371
      ...
      0.002564
      -0.022910
      -0.031687
      0.072002
      -0.008174
      -0.418219
      -0.225593
      -0.005716
      0.033955
      0.039606

      2
      2.176318
      0.396428
      0.302057
      -0.117164
      -0.006330
      0.077833
      0.000836
      0.064654
      -0.018309
      1.062417
      ...
      0.036894
      -0.008054
      0.026507
      0.053735
      0.025591
      0.024825
      0.169800
      0.061687
      0.028985
      -0.243151

      3
      0.194185
      0.155507
      0.046863
      0.047477
      -0.014495
      0.247765
      -0.057580
      -0.006338
      0.007387
      -0.423240
      ...
      -0.049155
      -0.010652
      0.007342
      -0.005267
      -0.031352
      -0.166973
      0.022989
      -0.033161
      -0.011156
      -0.129075

      4
      0.243474
      -0.491501
      -0.008307
      0.139973
      -0.204174
      1.664607
      -0.133342
      -0.047117
      -0.118995
      0.129404
      ...
      0.054157
      0.065400
      0.004748
      -0.072018
      -0.106567
      -0.590538
      0.219853
      -0.062958
      0.105441
      0.009634

5 rows × 3706 columns

```python
def recommend_movies(predictions_df, userID, movies_df, original_ratings_df, num_recommendations=5):

    # Get and sort the user's predictions
    user_row_number = userID - 1 # UserID starts at 1, not 0
    sorted_user_predictions = preds_df.iloc[user_row_number].sort_values(ascending=False) # UserID starts at 1

    # Get the user's data and merge in the movie information.
    user_data = original_ratings_df[original_ratings_df.UserID == (userID)]
    user_full = (user_data.merge(movies_df, how = 'left', left_on = 'MovieID', right_on = 'MovieID').
                     sort_values(['Rating'], ascending=False)
                 )

    print 'User {0} has already rated {1} movies.'.format(userID, user_full.shape[0])
    print 'Recommending highest {0} predicted ratings movies not already rated.'.format(num_recommendations)

    # Recommend the highest predicted rating movies that the user hasn't seen yet.
    recommendations = (movies_df[~movies_df['MovieID'].isin(user_full['MovieID'])].
         merge(pd.DataFrame(sorted_user_predictions).reset_index(), how = 'left',
               left_on = 'MovieID',
               right_on = 'MovieID').
         rename(columns = {user_row_number: 'Predictions'}).
         sort_values('Predictions', ascending = False).
                       iloc[:num_recommendations, :-1]
                      )

    return user_full, recommendations
```

```python
already_rated, predictions = recommend_movies(preds_df, 500, movies_df, ratings_df, 10)
```

    User 500 has already rated 101 movies.
    Recommending highest 10 predicted ratings movies not already rated.

So, how'd I do?

```python
already_rated.head(10)
```

      UserID
      MovieID
      Rating
      Timestamp
      Title
      Genres

      22
      500
      919
      5
      976643194
      Wizard of Oz, The (1939)
      Adventure|Children's|Drama|Musical

      65
      500
      3396
      5
      976289046
      Muppet Movie, The (1979)
      Children's|Comedy

      64
      500
      318
      5
      976643554
      Shawshank Redemption, The (1994)
      Drama

      24
      500
      953
      5
      976643741
      It's a Wonderful Life (1946)
      Drama

      63
      500
      2571
      5
      976644171
      Matrix, The (1999)
      Action|Sci-Fi|Thriller

      59
      500
      2700
      5
      976289080
      South Park: Bigger, Longer and Uncut (1999)
      Animation|Comedy

      28
      500
      3408
      5
      979257574
      Erin Brockovich (2000)
      Drama

      56
      500
      899
      5
      976643194
      Singin' in the Rain (1952)
      Musical|Romance

      30
      500
      3429
      5
      976288885
      Creature Comforts (1990)
      Animation|Comedy

      32
      500
      2804
      5
      976288902
      Christmas Story, A (1983)
      Comedy|Drama

One can download above code using my GITHUB Repository:https://github.com/ChintooIT/Streaming-Recommender-System