#  EPA-1316 Introduction to *Urban* Data Science

## Lab 6: plotting, Simple Linear Regression,K-NN Regression

**TU Delft**

**Q1 2020**

**Instructor:** Trivik Verma

**TAs:** Aarthi Meenakshi Sundaram, Jelle Egbers, Tess Kim, Lotte Lourens, Amir Ebrahimi Fard, Giulia Reggiani, Bramka Jafino, Talia Kaufmann

**[Computational Urban Science & Policy Lab](https://research.trivikverma.com/)**

---

##  Repetition of Content

Some important things in this lab that we have addressed before:

* Another example to illustrate the difference between `.iloc` and `.loc` in `pandas` -- > [here](#iloc)
* Some notes on why we are adding a constant in our linear regression model --> [here](#constant)

---

## Learning Goals

By the end of this lab, you should be able to:
* Review `numpy` including 2-D arrays and understand array reshaping
* Use `matplotlib` to make plots
* Feel comfortable with simple linear regression
* Feel comfortable with $k$ nearest neighbors

**This lab corresponds to Week 6.**

## Table of Contents

####  HIGHLIGHTS FROM PRE-LAB

* [1 - Review of numpy](#first-bullet)
* [2 - Another Intro to matplotlib plus more ](#second-bullet)

####  LAB 6 MATERIAL

* [3 - Simple Linear Regression](#third-bullet)
* [4 - Building a model with `statsmodels` and `sklearn`](#fourth-bullet)
* 5 - Example: Simple linear regression with automobile data - as a homework
* [6 - $k$Nearest Neighbors](#sixth-bullet)

```python
import numpy as np
import scipy as sp
import matplotlib as mpl
import matplotlib.cm as cm
import matplotlib.pyplot as plt
import pandas as pd
import time
pd.set_option('display.width', 500)
pd.set_option('display.max_columns', 100)
pd.set_option('display.notebook_repr_html', True)
#import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# Displays the plots for us.
%matplotlib inline
```

## 1 - Review of  the  `numpy` Python library

In lab1 we learned about the `numpy` library [(documentation)](http://www.numpy.org/) and its fast array structure, called the `numpy array`.

```python
# import numpy
import numpy as np
```

```python
# make an array
my_array = np.array([1,4,9,16])
my_array
```

    array([ 1,  4,  9, 16])

```python
print(f'Size of my array: {my_array.size}, or length of my array: {len(my_array)}')
print (f'Shape of my array: {my_array.shape}')
```

    Size of my array: 4, or length of my array: 4
    Shape of my array: (4,)

#### Notice the way the shape appears in numpy arrays

- For a 1D array, .shape returns a tuple with 1 element (n,)
- For a 2D array, .shape returns a tuple with 2 elements (n,m)
- For a 3D array, .shape returns a tuple with 3 elements (n,m,p)

```python
# How to reshape a 1D array to a 2D
my_array.reshape(-1,2)
```

    array([[ 1,  4],
           [ 9, 16]])

Numpy arrays support the same operations as lists! Below we slice and iterate.

```python
print("array[2:4]:", my_array[2:4]) # A slice of the array

# Iterate over the array
for ele in my_array:
    print("element:", ele)
```

    array[2:4]: [ 9 16]
    element: 1
    element: 4
    element: 9
    element: 16

Remember `numpy` gains a lot of its efficiency from being **strongly typed** (all elements are of the same type, such as integer or floating point). If the elements of an array are of a different type, `numpy` will force them into the same type (the longest in terms of bytes)

```python
mixed = np.array([1, 2.3, 'eleni', True])
print(type(1), type(2.3), type('eleni'), type(True))
mixed # all elements will become strings
```

    <class 'int'> <class 'float'> <class 'str'> <class 'bool'>

    array(['1', '2.3', 'eleni', 'True'], dtype='<U32')

Next, we push ahead to two-dimensional arrays and begin to dive into some of the deeper aspects of `numpy`.

```python
# create a 2d-array by handing a list of lists
my_array2d = np.array([ [1, 2, 3, 4],
                        [5, 6, 7, 8],
                        [9, 10, 11, 12]
])

my_array2d
```

    array([[ 1,  2,  3,  4],
           [ 5,  6,  7,  8],
           [ 9, 10, 11, 12]])

### Array Slicing (a reminder...)

Numpy arrays can be sliced, and can be iterated over with loops.  Below is a schematic illustrating slicing two-dimensional arrays.

Notice that the list slicing syntax still works!
`array[2:,3]` says "in the array, get rows 2 through the end, column 3]"
`array[3,:]` says "in the array, get row 3, all columns".

### Pandas Slicing (a reminder...)

`.iloc` is by position (position is unique), `.loc` is by label (label is not unique)

```python
# import cast dataframe
cast = pd.read_csv('data/cast.csv', encoding='utf_8')
cast.head()
```

      title
      year
      name
      type
      character
      n

      0
      Closet Monster
      2015
      Buffy #1
      actor
      Buffy 4
      31.0

      1
      Suuri illusioni
      1985
      Homo $
      actor
      Guests
      22.0

      2
      Battle of the Sexes
      2017
      $hutter
      actor
      Bobby Riggs Fan
      10.0

      3
      Secret in Their Eyes
      2015
      $hutter
      actor
      2002 Dodger Fan
      NaN

      4
      Steve Jobs
      2015
      $hutter
      actor
      1988 Opera House Patron
      NaN

```python
# get me rows 10 to 13 (python slicing style : exclusive of end)
cast.iloc[10:13]
```

      title
      year
      name
      type
      character
      n

      10
      When the Man Went South
      2014
      Taipaleti 'Atu'ake
      actor
      Two Palms - Ua'i Paame
      8.0

      11
      Little Angel (Angelita)
      2015
      Michael 'babeepower' Viera
      actor
      Chico
      9.0

      12
      Mixing Nia
      1998
      Michael 'babeepower' Viera
      actor
      Rapper
      NaN

```python
# get me columns 0 to 2 but all rows - use head()
cast.iloc[:, 0:2].head()
```

      title
      year

      0
      Closet Monster
      2015

      1
      Suuri illusioni
      1985

      2
      Battle of the Sexes
      2017

      3
      Secret in Their Eyes
      2015

      4
      Steve Jobs
      2015

```python
# get me rows 10 to 13 AND only columns 0 to 2
cast.iloc[10:13, 0:2]
```

      title
      year

      10
      When the Man Went South
      2014

      11
      Little Angel (Angelita)
      2015

      12
      Mixing Nia
      1998

```python
# COMPARE: get me rows 10 to 13 (pandas slicing style : inclusive of end)
cast.loc[10:13]
```

      title
      year
      name
      type
      character
      n

      10
      When the Man Went South
      2014
      Taipaleti 'Atu'ake
      actor
      Two Palms - Ua'i Paame
      8.0

      11
      Little Angel (Angelita)
      2015
      Michael 'babeepower' Viera
      actor
      Chico
      9.0

      12
      Mixing Nia
      1998
      Michael 'babeepower' Viera
      actor
      Rapper
      NaN

      13
      The Replacements
      2000
      Steven 'Bear'Boyd
      actor
      Defensive Tackle - Washington Sentinels
      NaN

```python
# give me columns 'year' and 'type' by label but only for rows 5 to 10
cast.loc[5:10,['year','type']]
```

      year
      type

      5
      2015
      actor

      6
      2015
      actor

      7
      2009
      actor

      8
      2014
      actor

      9
      2014
      actor

      10
      2014
      actor

#### Another example of positioning with `.iloc` and `loc`

Look at the following data frame. It is a bad example because we have duplicate values for the index but that is legal in pandas. It's just a bad practice and we are doing it to illustrate the difference between positioning with `.iloc` and `loc`. To keep rows unique, though, internally, `pandas` has its own index which in this dataframe runs from `0` to `2`.

```python
index = ['A', 'Z', 'A']
famous = pd.DataFrame({'Elton': ['singer', 'Candle in the wind', 'male'],
                  'Maraie': ['actress' , 'Do not know', 'female'],
                  'num': np.random.randn(3)}, index=index)
famous
```

      Elton
      Maraie
      num

      A
      singer
      actress
      0.804906

      Z
      Candle in the wind
      Do not know
      -0.114201

      A
      male
      female
      -1.068322

```python
# accessing elements by label can bring up duplicates!!
famous.loc['A']  # since we want all rows is the same as famous.loc['A',:]
```

      Elton
      Maraie
      num

      A
      singer
      actress
      0.804906

      A
      male
      female
      -1.068322

```python
# accessing elements by position is unique - brings up only one row
famous.iloc[1]
```

    Elton     Candle in the wind
    Maraie           Do not know
    num                -0.114201
    Name: Z, dtype: object

## 2 - Plotting with matplotlib and beyond

`matplotlib` is a very powerful `python` library for making scientific plots.

We will not focus too much on the internal aspects of `matplotlib` in today's lab. There are many excellent tutorials out there for `matplotlib`.  For example,
* [`matplotlib` homepage](https://matplotlib.org/)
* [`matplotlib` tutorial](https://github.com/matplotlib/AnatomyOfMatplotlib)

Conveying your findings convincingly is an absolutely crucial part of any analysis. Therefore, you must be able to write well and make compelling visuals.  Creating informative visuals is an involved process and we won't cover that in this lab.  However, part of creating informative data visualizations means generating *readable* figures.  If people can't read your figures or have a difficult time interpreting them, they won't understand the results of your work.  Here are some non-negotiable commandments for any plot:
* Label $x$ and $y$ axes
* Axes labels should be informative
* Axes labels should be large enough to read
* Make tick labels large enough
* Include a legend if necessary
* Include a title if necessary
* Use appropriate line widths
* Use different line styles for different lines on the plot
* Use different markers for different lines

There are other important elements, but that list should get you started on your way.

We will work with `matplotlib` and `seaborn` for plotting in this class.  `matplotlib` is a very powerful `python` library for making scientific plots.  `seaborn` is a little more specialized in that it was developed for statistical data visualization. We have already covered `seaborn` in previous weeks. However, you can look at the [seaborn documentation](https://seaborn.pydata.org) for more.

First, let's generate some data.

#### Let's plot some functions (Don't worry about the math of these functions - they are examples)

We will use the following three functions to make some plots:

* Logistic function:
  \begin{align*}
    f\left(z\right) = \dfrac{1}{1 + be^{-az}}
  \end{align*}
  where $a$ and $b$ are parameters.
* Hyperbolic tangent:
  \begin{align*}
    g\left(z\right) = b\tanh\left(az\right) + c
  \end{align*}
  where $a$, $b$, and $c$ are parameters.
* Rectified Linear Unit:
  \begin{align*}
    h\left(z\right) =
    \left\{
      \begin{array}{lr}
        z, \quad z > 0 \\
        \epsilon z, \quad z\leq 0
      \end{array}
    \right.
  \end{align*}
  where $\epsilon < 0$ is a small, positive parameter.

You are given the code for the first two functions.  Notice that $z$ is passed in as a `numpy` array and that the functions are returned as `numpy` arrays.  Parameters are passed in as floats.

You should write a function to compute the rectified linear unit.  The input should be a `numpy` array for $z$ and a positive float for $\epsilon$.

```python
import numpy as np

def logistic(z: np.ndarray, a: float, b: float) -> np.ndarray:
    """ Compute logistic function
      Inputs:
         a: exponential parameter
         b: exponential prefactor
         z: numpy array; domain
      Outputs:
         f: numpy array of floats, logistic function
    """

    den = 1.0 + b * np.exp(-a * z)
    return 1.0 / den

def stretch_tanh(z: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
    """ Compute stretched hyperbolic tangent
      Inputs:
         a: horizontal stretch parameter (a>1 implies a horizontal squish)
         b: vertical stretch parameter
         c: vertical shift parameter
         z: numpy array; domain
      Outputs:
         g: numpy array of floats, stretched tanh
    """
    return b * np.tanh(a * z) + c

def relu(z: np.ndarray, eps: float = 0.01) -> np.ndarray:
    """ Compute rectificed linear unit
      Inputs:
         eps: small positive parameter
         z: numpy array; domain
      Outputs:
         h: numpy array; relu
    """
    return np.fmax(z, eps * z)
```

Now let's make some plots.  First, let's just warm up and plot the logistic function.

```python
x = np.linspace(-5.0, 5.0, 100) # Equally spaced grid of 100 pts between -5 and 5

f = logistic(x, 1.0, 1.0) # Generate data
```

```python
plt.plot(x, f)
plt.xlabel('x')
plt.ylabel('f')
plt.title('Logistic Function')
plt.grid(True)
```

#### Figures with subplots

Let's start thinking about the plots as objects. We have the `figure` object which is like a matrix of smaller plots named `axes`. You can use array notation when handling it.

```python
fig, ax = plt.subplots(1,1) # Get figure and axes objects

ax.plot(x, f) # Make a plot

# Create some labels
ax.set_xlabel('x')
ax.set_ylabel('f')
ax.set_title('Logistic Function')

# Grid
ax.grid(True)
```

Wow, it's *exactly* the same plot!  Notice, however, the use of `ax.set_xlabel()` instead of `plt.xlabel()`.  The difference is tiny, but you should be aware of it.  I will use this plotting syntax from now on.

What else do we need to do to make this figure better?  Here are some options:
* Make labels bigger!
* Make line fatter
* Make tick mark labels bigger
* Make the grid less pronounced
* Make figure bigger

Let's get to it.

```python
fig, ax = plt.subplots(1,1, figsize=(10,6)) # Make figure bigger

# Make line plot
ax.plot(x, f, lw=4)

# Update ticklabel size
ax.tick_params(labelsize=24)

# Make labels
ax.set_xlabel(r'$x$', fontsize=24) # Use TeX for mathematical rendering
ax.set_ylabel(r'$f(x)$', fontsize=24) # Use TeX for mathematical rendering
ax.set_title('Logistic Function', fontsize=24)

ax.grid(True, lw=1.5, ls='--', alpha=0.75)
```

Notice:
* `lw` stands for `linewidth`.  We could also write `ax.plot(x, f, linewidth=4)`
* `ls` stands for `linestyle`.
* `alpha` stands for transparency.

The only thing remaining to do is to change the $x$ limits.  Clearly these should go from $-5$ to $5$.

```python
#fig.savefig('figs/logistic.png')

# Put this in a markdown cell and uncomment this to check what you saved.
#
```

#### Resources
If you want to see all the styles available, please take a look at the documentation.
* [Line styles](https://matplotlib.org/2.0.1/api/lines_api.html#matplotlib.lines.Line2D.set_linestyle)
* [Marker styles](https://matplotlib.org/2.0.1/api/markers_api.html#module-matplotlib.markers)
* [Everything you could ever want](https://matplotlib.org/2.0.1/api/lines_api.html#matplotlib.lines.Line2D.set_marker)

We haven't discussed it yet, but you can also put a legend on a figure. Here are some additional resources:
* [Legend](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html)
* [Grid](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.grid.html)

`ax.legend(loc='best', fontsize=24);`

## 3 - Simple Linear Regression

Linear regression and its many extensions are a workhorse of the statistics and data science community, both in application and as a reference point for other models. Most of the major concepts in machine learning can be and often are discussed in terms of various linear regression models. Thus, this section will introduce you to building and fitting linear regression models and some of the process behind it, so that you can 1) fit models to data you encounter 2) experiment with different kinds of linear regression and observe their effects 3) see some of the technology that makes regression models work.

### Linear regression with a toy dataset
We first examine a toy problem, focusing our efforts on fitting a linear model to a small dataset with three observations.  Each observation consists of one predictor $x_i$ and one response $y_i$ for $i = 1, 2, 3$,

\begin{align*}
(x , y) = \{(x_1, y_1), (x_2, y_2), (x_3, y_3)\}.
\end{align*}

To be very concrete, let's set the values of the predictors and responses.

\begin{equation*}
(x , y) = \{(1, 2), (2, 2), (3, 4)\}
\end{equation*}

There is no line of the form $\beta_0 + \beta_1 x = y$ that passes through all three observations, since the data are not collinear. Thus our aim is to find the line that best fits these observations in the *least-squares sense*, as discussed in lecture.

* Make two numpy arrays out of this data, x_train and y_train
* Check the dimentions of these arrays
* Try to reshape them into a different shape
* Make points into a very simple scatterplot
* Make a better scatterplot

```python
x_train = np.array([1,2,3])
y_train = np.array([2,3,6])
type(x_train)
```

    numpy.ndarray

```python
x_train.shape
```

    (3,)

```python
x_train = x_train.reshape(3,1)
x_train.shape
```

    (3, 1)

```python
# Make a simple scatterplot
plt.scatter(x_train,y_train)

# check dimensions
print(x_train.shape,y_train.shape)

```

```python
def nice_scatterplot(x, y, title):
    # font size
    f_size = 18

    # make the figure
    fig, ax = plt.subplots(1,1, figsize=(8,5)) # Create figure object

    # set axes limits to make the scale nice
    ax.set_xlim(np.min(x)-1, np.max(x) + 1)
    ax.set_ylim(np.min(y)-1, np.max(y) + 1)

    # adjust size of tickmarks in axes
    ax.tick_params(labelsize = f_size)

    # remove tick labels
    ax.tick_params(labelbottom=False,  bottom=False)

    # adjust size of axis label
    ax.set_xlabel(r'$x$', fontsize = f_size)
    ax.set_ylabel(r'$y$', fontsize = f_size)

    # set figure title label
    ax.set_title(title, fontsize = f_size)

    # you may set up grid with this
    ax.grid(True, lw=1.75, ls='--', alpha=0.15)

    # make actual plot (Notice the label argument!)
    #ax.scatter(x, y, label=r'$my points$')
    #ax.scatter(x, y, label='$my points$')
    ax.scatter(x, y, label=r'$my\,points$')
    ax.legend(loc='best', fontsize = f_size);

    return ax

nice_scatterplot(x_train, y_train, 'hello nice plot')

```

#### Formulae
Linear regression is special among the models we study because it can be solved explicitly. While most other models (and even some advanced versions of linear regression) must be solved itteratively, linear regression has a formula where you can simply plug in the data.

For the single predictor case it is:
    \begin{align}
      \beta_1 &= \frac{\sum_{i=1}^n{(x_i-\bar{x})(y_i-\bar{y})}}{\sum_{i=1}^n{(x_i-\bar{x})^2}}\\
      \beta_0 &= \bar{y} - \beta_1\bar{x}\
    \end{align}

Where $\bar{y}$ and $\bar{x}$ are the mean of the y values and the mean of the x values, respectively.

###  Building a model from scratch
In this part, we will solve the equations for simple linear regression and find the best fit solution to our toy problem.

The snippets of code below implement the linear regression equations on the observed predictors and responses, which we'll call the training data set.  Let's walk through the code.

We have to reshape our arrrays to 2D. We will see later why.

* make an array with shape (2,3)
* reshape it to a size that you want

```python
xx = np.array([[1,2,3],[4,6,8]])
xxx = xx.reshape(-1,2)
xxx.shape
```

    (3, 2)

```python
# Reshape to be a proper 2D array
x_train = x_train.reshape(x_train.shape[0], 1)
y_train = y_train.reshape(y_train.shape[0], 1)

print(x_train.shape)
```

    (3, 1)

```python
# first, compute means
y_bar = np.mean(y_train)
x_bar = np.mean(x_train)

# build the two terms
numerator = np.sum( (x_train - x_bar)*(y_train - y_bar) )
denominator = np.sum((x_train - x_bar)**2)

print(numerator.shape, denominator.shape) #check shapes
```

    () ()

* Why the empty brackets? (The numerator and denominator are scalars, as expected.)

```python
#slope beta1
beta_1 = numerator/denominator

#intercept beta0
beta_0 = y_bar - beta_1*x_bar

print("The best-fit line is {0:3.2f} + {1:3.2f} * x".format(beta_0, beta_1))
print(f'The best fit is {beta_0}')
```

    The best-fit line is -0.33 + 2.00 * x
    The best fit is -0.3333333333333335

Exercise

Turn the code from the above cells into a function called `simple_linear_regression_fit`, that inputs the training data and returns `beta0` and `beta1`.

To do this, copy and paste the code from the above cells below and adjust the code as needed, so that the training data becomes the input and the betas become the output.

```python
def simple_linear_regression_fit(x_train: np.ndarray, y_train: np.ndarray) -> np.ndarray:

    return
```

Check your function by calling it with the training data from above and printing out the beta values.

```python
# Your code here
# First try it yourself and if it dosesnt work, try again. Stil doesn't work? Try working with a friend in groups. Nothing? Ok, use the code below.

```

```python
# %load solutions/simple_linear_regression_fit.py

```

* Let's run this function and see the coefficients

```python
x_train = np.array([1 ,2, 3])
y_train = np.array([2, 2, 4])

betas = simple_linear_regression_fit(x_train, y_train)

beta_0 = betas[0]
beta_1 = betas[1]

print("The best-fit line is {0:8.6f} + {1:8.6f} * x".format(beta_0, beta_1))
```

    Reshaping features array.
    Reshaping observations array.
    The best-fit line is 0.666667 + 1.000000 * x

Exercise

* Do the values of `beta0` and `beta1` seem reasonable?
* Plot the training data using a scatter plot.
* Plot the best fit line with `beta0` and `beta1` together with the training data.

```python
# Your code here
# First try it yourself and if it dosesnt work, try again. Stil doesn't work? Try working with a friend in groups. Nothing? Ok, use the code below.

```

```python
# %load solutions/best_fit_scatterplot.py

```

The values of `beta0` and `beta1` seem roughly reasonable.  They capture the positive correlation.  The line does appear to be trying to get as close as possible to all the points.

## 4 - Building a model with `statsmodels` and `sklearn`

Now that we can concretely fit the training data from scratch, let's learn two `python` packages to do it all for us:
* [statsmodels](http://www.statsmodels.org/stable/regression.html) and
* [scikit-learn (sklearn)](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html).

Our goal  is to show how to implement simple linear regression with these packages.  For an important sanity check, we compare the $\beta$ values from `statsmodels` and `sklearn` to the $\beta$ values that we found from above with our own implementation.

For the purposes of this lab, `statsmodels` and `sklearn` do the same thing.  More generally though, `statsmodels` tends to be easier for inference \[finding the values of the slope and intercept and dicussing uncertainty in those values\], whereas `sklearn` has machine-learning algorithms and is better for prediction \[guessing y values for a given x value\]. (Note that both packages make the same guesses, it's just a question of which activity they provide more support for.

**Note:** `statsmodels` and `sklearn` are different packages!  Unless we specify otherwise, you can use either one.

### Why do we need to add a constant in our simple linear regression model?

Let's say we a data set of two obsevations with one predictor and one response variable each. We would then have the following two equations if we run a simple linear regression model. $$y_1=\beta_0 + \beta_1*x_1$$ $$y_2=\beta_0 + \beta_1*x_2$$
 For simplicity and calculation efficiency we want to "absorb" the constant $b_0$ into an array with $b_1$ so we have only multiplication. To do this we introduce the constant ${x}^0=1$
$$y_1=\beta_0*{x_1}^0 + \beta_1*x_1$$ $$y_2=\beta_0 * {x_2}^0 + \beta_1*x_2$$
 That becomes:
$$y_1=\beta_0*1 + \beta_1*x_1$$ $$y_2=\beta_0 * 1 + \beta_1*x_2$$

In matrix notation:

$$
\left [
\begin{array}{c}
y_1 \\ y_2 \\
\end{array}
\right] =
\left [
\begin{array}{cc}
1& x_1 \\ 1 & x_2 \\
\end{array}
\right]
\cdot
\left [
\begin{array}{c}
\beta_0 \\ \beta_1 \\
\end{array}
\right]
$$

`sklearn` adds the constant for us where in `statsmodels` we need to explicitly add it using `sm.add_constant`

Below is the code for `statsmodels`.  `Statsmodels` does not by default include the column of ones in the $X$ matrix, so we include it manually with `sm.add_constant`.

```python
import statsmodels.api as sm
```

```python
# create the X matrix by appending a column of ones to x_train
X = sm.add_constant(x_train)

# this is the same matrix as in our scratch problem!
print(X)

# build the OLS model (ordinary least squares) from the training data
toyregr_sm = sm.OLS(y_train, X)

# do the fit and save regression info (parameters, etc) in results_sm
results_sm = toyregr_sm.fit()

# pull the beta parameters out from results_sm
beta0_sm = results_sm.params[0]
beta1_sm = results_sm.params[1]

print(f'The regression coef from statsmodels are: beta_0 = {beta0_sm:8.6f} and beta_1 = {beta1_sm:8.6f}')
```

    [[1. 1.]
     [1. 2.]
     [1. 3.]]
    The regression coef from statsmodels are: beta_0 = 0.666667 and beta_1 = 1.000000

Besides the beta parameters, `results_sm` contains a ton of other potentially useful information.

```python
import warnings
warnings.filterwarnings('ignore')
print(results_sm.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      y   R-squared:                       0.750
    Model:                            OLS   Adj. R-squared:                  0.500
    Method:                 Least Squares   F-statistic:                     3.000
    Date:                Thu, 17 Sep 2020   Prob (F-statistic):              0.333
    Time:                        11:43:06   Log-Likelihood:                -2.0007
    No. Observations:                   3   AIC:                             8.001
    Df Residuals:                       1   BIC:                             6.199
    Df Model:                           1
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.6667      1.247      0.535      0.687     -15.181      16.514
    x1             1.0000      0.577      1.732      0.333      -6.336       8.336
    ==============================================================================
    Omnibus:                          nan   Durbin-Watson:                   3.000
    Prob(Omnibus):                    nan   Jarque-Bera (JB):                0.531
    Skew:                          -0.707   Prob(JB):                        0.767
    Kurtosis:                       1.500   Cond. No.                         6.79
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Now let's turn our attention to the `sklearn` library.

```python
from sklearn import linear_model
```

```python
# build the least squares model
toyregr = linear_model.LinearRegression()

# save regression info (parameters, etc) in results_skl
results = toyregr.fit(x_train, y_train)

# pull the beta parameters out from results_skl
beta0_skl = toyregr.intercept_
beta1_skl = toyregr.coef_[0]

print("The regression coefficients from the sklearn package are: beta_0 = {0:8.6f} and beta_1 = {1:8.6f}".format(beta0_skl, beta1_skl))
```

    The regression coefficients from the sklearn package are: beta_0 = 0.666667 and beta_1 = 1.000000

We should feel pretty good about ourselves now, and we're ready to move on to a real problem!

### The `scikit-learn` library and the shape of things

Before diving into a "real" problem, let's discuss more of the details of `sklearn`.

`Scikit-learn` is the main `Python` machine learning library. It consists of many learners which can learn models from data, as well as a lot of utility functions such as `train_test_split()`.

Use the following to add the library into your code:

```python
import sklearn
```

In `scikit-learn`, an **estimator** is a Python object that implements the methods `fit(X, y)` and `predict(T)`

Let's see the structure of `scikit-learn` needed to make these fits. `fit()` always takes two arguments:
```python
estimator.fit(Xtrain, ytrain)
```
We will consider two estimators in this lab: `LinearRegression` and `KNeighborsRegressor`.

It is very important to understand that `Xtrain` must be in the form of a **2x2 array** with each row corresponding to one sample, and each column corresponding to the feature values for that sample.

`ytrain` on the other hand is a simple array of responses.  These are continuous for regression problems.

### Practice with `sklearn` and a real dataset
We begin by loading up the `mtcars` dataset. This data was extracted from the 1974 Motor Trend US magazine, and comprises of fuel consumption and 10 aspects of automobile design and performance for 32 automobiles (1973–74 models). We will load this data to a dataframe with 32 observations on 11 (numeric) variables. Here is an explanation of the features:

- `mpg` is Miles/(US) gallon
- `cyl` is Number of cylinders,
- `disp` is	Displacement (cu.in.),
- `hp` is	Gross horsepower,
- `drat` is	Rear axle ratio,
- `wt` is the Weight (1000 lbs),
- `qsec` is 1/4 mile time,
- `vs` is Engine (0 = V-shaped, 1 = straight),
- `am` is Transmission (0 = automatic, 1 = manual),
- `gear` is the Number of forward gears,
- `carb` is	Number of carburetors.

```python
import pandas as pd

#load mtcars
dfcars = pd.read_csv("data/mtcars.csv")
dfcars.head()
```

      Unnamed: 0
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      1
      Mazda RX4 Wag
      21.0
      6
      160.0
      110
      3.90
      2.875
      17.02
      0
      1
      4
      4

      2
      Datsun 710
      22.8
      4
      108.0
      93
      3.85
      2.320
      18.61
      1
      1
      4
      1

      3
      Hornet 4 Drive
      21.4
      6
      258.0
      110
      3.08
      3.215
      19.44
      1
      0
      3
      1

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

```python
# Fix the column title
dfcars = dfcars.rename(columns={"Unnamed: 0":"car name"})
dfcars.head()
```

      car name
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      1
      Mazda RX4 Wag
      21.0
      6
      160.0
      110
      3.90
      2.875
      17.02
      0
      1
      4
      4

      2
      Datsun 710
      22.8
      4
      108.0
      93
      3.85
      2.320
      18.61
      1
      1
      4
      1

      3
      Hornet 4 Drive
      21.4
      6
      258.0
      110
      3.08
      3.215
      19.44
      1
      0
      3
      1

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

```python
dfcars.shape
```

    (32, 12)

#### Searching for values: how many cars have 4 gears?

```python
len(dfcars[dfcars.gear == 4].drop_duplicates(subset='car name', keep='first'))
```

    12

Next, let's split the dataset into a training set and test set.

```python
# split into training set and testing set
from sklearn.model_selection import train_test_split

#set random_state to get the same split every time
traindf, testdf = train_test_split(dfcars, test_size=0.2, random_state=42)
```

```python
# testing set is around 20% of the total data; training set is around 80%
print("Shape of full dataset is: {0}".format(dfcars.shape))
print("Shape of training dataset is: {0}".format(traindf.shape))
print("Shape of test dataset is: {0}".format(testdf.shape))
```

    Shape of full dataset is: (32, 12)
    Shape of training dataset is: (25, 12)
    Shape of test dataset is: (7, 12)

Now we have training and test data.  We still need to select a predictor and a response from this dataset.  Keep in mind that we need to choose the predictor and response from both the training and test set.  You will do this in the exercises below.  However, we provide some starter code for you to get things going.

```python
traindf.head()
```

      car name
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      25
      Fiat X1-9
      27.3
      4
      79.0
      66
      4.08
      1.935
      18.90
      1
      1
      4
      1

      12
      Merc 450SL
      17.3
      8
      275.8
      180
      3.07
      3.730
      17.60
      0
      0
      3
      3

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

      16
      Chrysler Imperial
      14.7
      8
      440.0
      230
      3.23
      5.345
      17.42
      0
      0
      3
      4

```python
# Extract the response variable that we're interested in
y_train = traindf.mpg
y_train
```

    25    27.3
    12    17.3
    0     21.0
    4     18.7
    16    14.7
    5     18.1
    13    15.2
    11    16.4
    23    13.3
    1     21.0
    2     22.8
    26    26.0
    3     21.4
    21    15.5
    27    30.4
    22    15.2
    18    30.4
    31    21.4
    20    21.5
    7     24.4
    10    17.8
    14    10.4
    28    15.8
    19    33.9
    6     14.3
    Name: mpg, dtype: float64

Exercise

Use slicing to get the same vector `y_train`

----

Now, notice the shape of `y_train`.

```python
y_train.shape, type(y_train)
```

    ((25,), pandas.core.series.Series)

### Array reshape
This is a 1D array as should be the case with the **Y** array.  Remember, `sklearn` requires a 2D array only for the predictor array.  You will have to pay close attention to this in the exercises later. `Sklearn` doesn't care too much about the shape of `y_train`.

The whole reason we went through that whole process was to show you how to reshape your data into the correct format.

**IMPORTANT:** Remember that your response variable `ytrain` can be a vector but your predictor variable `xtrain` ***must*** be an array!

## 5 - Example: Simple linear regression with automobile data - as a homework

To get to part 6 below, you will have to first complete the homework exercise 4 and then use that to complete k-nearest neighbours exercise below. The variables x_train and y_train come from the homework and it is your task to do it. If you are not able to, then solutions will be provided to you.

## 6 - $k$-nearest neighbors

Now that you're familiar with `sklearn`, you're ready to do a KNN regression.

Sklearn's regressor is called `sklearn.neighbors.KNeighborsRegressor`. Its main parameter is the `number of nearest neighbors`. There are other parameters such as the distance metric (default for 2 order is the Euclidean distance). For a list of all the parameters see the [Sklearn kNN Regressor Documentation](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html).

Let's use $5$ nearest neighbors.

```python
# Import the library
from sklearn.neighbors import KNeighborsRegressor
```

```python
# Set number of neighbors
k = 5
knnreg = KNeighborsRegressor(n_neighbors=k)
```

```python
# Fit the regressor - make sure your numpy arrays are the right shape
knnreg.fit(X_train, y_train)

# Evaluate the outcome on the train set using R^2
r2_train = knnreg.score(X_train, y_train)

# Print results
print(f'kNN model with {k} neighbors gives R^2 on the train set: {r2_train:.5}')
```

    kNN model with 5 neighbors gives R^2 on the train set: 0.87181

```python
knnreg.predict(X_test)
```

    array([20.14, 14.  , 15.3 , 26.3 , 19.56, 17.06, 16.88])

Exercise

Calculate and print the $R^{2}$ score on the test set

```python
r2_test = knnreg.score(X_test, y_test)
print(f'kNN model with {k} neighbors gives R^2 on the test set: {r2_test:.5}')
```

    kNN model with 5 neighbors gives R^2 on the test set: 0.69922

Not so good? Lets vary the number of neighbors and see what we get.

```python
# Make our lives easy by storing the different regressors in a dictionary
regdict = {}

# Make our lives easier by entering the k values from a list
k_list = [1, 2, 4, 15]

# Do a bunch of KNN regressions
for k in k_list:
    knnreg = KNeighborsRegressor(n_neighbors=k)
    knnreg.fit(X_train, y_train)
    # Store the regressors in a dictionary
    regdict[k] = knnreg

# Print the dictionary to see what we have
regdict
```

    {1: KNeighborsRegressor(n_neighbors=1),
     2: KNeighborsRegressor(n_neighbors=2),
     4: KNeighborsRegressor(n_neighbors=4),
     15: KNeighborsRegressor(n_neighbors=15)}

Now let's plot all the k values in same plot.

```python
fig, ax = plt.subplots(1,1, figsize=(10,6))

ax.plot(dfcars.wt, dfcars.mpg, 'o', label="data")

xgrid = np.linspace(np.min(dfcars.wt), np.max(dfcars.wt), 100)

# let's unpack the dictionary to its elements (items) which is the k and Regressor
for k, regressor in regdict.items():
    predictions = regressor.predict(xgrid.reshape(-1,1))
    ax.plot(xgrid, predictions, label="{}-NN".format(k))

ax.legend();
```

Exercise

Explain what you see in the graph. **Hint** Notice how the $1$-NN goes through every point on the training set but utterly fails elsewhere.

Lets look at the scores on the training set.

```python
ks = range(1, 15) # Grid of k's
scores_train = [] # R2 scores
for k in ks:
    # Create KNN model
    knnreg = KNeighborsRegressor(n_neighbors=k)

    # Fit the model to training data
    knnreg.fit(X_train, y_train)

    # Calculate R^2 score
    score_train = knnreg.score(X_train, y_train)
    scores_train.append(score_train)

# Plot
fig, ax = plt.subplots(1,1, figsize=(12,8))
ax.plot(ks, scores_train,'o-')
ax.set_xlabel(r'$k$')
ax.set_ylabel(r'$R^{2}$')
```

Exercise

* Why do we get a perfect $R^2$ at k=1 for the training set?
* Make the same plot as above on the *test* set.
* What is the best $k$?

```python
# Your code here
# First try it yourself and if it dosesnt work, try again. Stil doesn't work? Try working with a friend in groups. Nothing? Ok, use the code below.

```

```python
# %load solutions/knn_regression.py

```


# 823 HW2
## https://yiyangzhang2020.github.io/yz628-823-blog/

## Number theory and a Google recruitment puzzle
### Find the first 10-digit prime in the decimal expansion of 17π.
### The first 5 digits in the decimal expansion of π are 14159. The first 4-digit prime in the decimal expansion of π are 4159. You are asked to find the first 10-digit prime in the decimal expansion of 17π. First solve sub-problems (divide and conquer):

### -------------------------------------------------------------------------------------------------------
### Write a function to generate an arbitrary large expansion of a mathematical expression like π.
#### (Hint: You can use the standard library `decimal` or the 3rd party library `sympy` to do this)

### For the first function, I used the sympy library (mpmath). Since the function wasn't outputting enough decimal places, I set the mp.dps as 1000.
### This function has two input conditions to check, either pi(π) or e(euler number). Next, I set a multiplier and multiply them by it. Then I set the precision criterion to get the number of digits after the decimal wanted and turn it into a string. This creates the decimal expansion of a given number.

```python
import math
try:
    from sympy.mpmath import mp
except ImportError:
    from mpmath import mp
mp.dps=1000

def create_expansion(precision,number,multiplier):
     '''
    this function takes a number with a multiplier and outputs its decimal expansion with a specific number of decimals

    input:
    precision, number of decimals wanted
    number, number to be expanded('pi' or 'e')
    multiplier, multiplier of the number to be expanded

    returns: an string of decimal expansion of the input number
    '''
    #check if number is pi
    if number =='pi':
        #create a string of the number(multiplied by the multiplier) expansion with precision as number of decimals
        str_pi = str(( multiplier*mp.pi)).replace('.','')[0:precision]
        return(str_pi)
    #check if number is e
    elif number =='e':
        str_e = str(( multiplier*mp.e)).replace('.','')[0:precision]
        return(str_e)

#type either 'pi' or 'e' for number in create_expansion function

#print(create_expansion(50,'pi',17))
#print(create_expansion(25,'e',1))
```

### Unit test of create_expansion function:

```python
import unittest

class TestNotebook(unittest.TestCase):

    def test_create_expansion(self):
        """test the expansion of the number we want"""
        self.assertEqual(create_expansion(5,'pi',1),str(31415))

unittest.main(argv=[''], verbosity=2, exit=False)
```

    test_create_expansion (__main__.TestNotebook)
    test the expansion of the number we want ... ok

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK

    <unittest.main.TestProgram at 0x7f8427f6b950>

### -------------------------------------------------------------------------------------------------------
### - Write a function to check if a number is prime. Hint: See Sieve of Eratosthenes

### For this function, the first criterion I set is to check if the given number is 1 or not. If it is 1, then it is not a prime number.
### Next we check if the given number is 2 or not. If it is 2, then it is a prime number.
### Then we check if the given number can be divided by 2, if so, it is an even number, thus it is not a prime number.
### Lastly, we check from 3 to the positive square root of x so that it only iterate a portion of X values. The step is 2 so no even number other than 2 will participate in this iteration.
### This function reduces the run time complexity dramatically from a function without the above steps.

```python
import math
def IsPrimeNumber(x):
    '''
    this function takes an input number and test whether it is a prime number or not
    and outputs an answer
    x: int, input to be tested
    returns: an answer (True or False)
    '''
    # exclude 1 which is not prime
    if x == 1:
        return False
    # take out 2 as a base case
    elif x == 2:
        return True
    elif x % 2 == 0:
        return False
    else:
        # iterate through 3 to the positive square root of x to see if x can be divided by any, step is 2 which excludes all even number.
        for y in range(3, int(math.sqrt(x) + 1), 2):
            # if x can be divided, then x is not prime
            if x % y == 0:
                return False
    # if x can not be divided, then x is a prime number
    return True
```

### Unit test of IsPrimeNumber function:

```python
class TestNotebook(unittest.TestCase):

    def test_IsPrimeNumber(self):
        """test IsPrimeNumber"""
        self.assertFalse(IsPrimeNumber(1))
        self.assertTrue(IsPrimeNumber(2))
        self.assertFalse(IsPrimeNumber(51))
        self.assertTrue(IsPrimeNumber(1373))
        self.assertFalse(IsPrimeNumber(33333))

unittest.main(argv=[''], verbosity=2, exit=False)
```

    test_IsPrimeNumber (__main__.TestNotebook)
    test IsPrimeNumber ... ok

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK

    <unittest.main.TestProgram at 0x7f84390c1d10>

### -------------------------------------------------------------------------------------------------------
### - Write a function to generate sliding windows of a specified width from a long iterable (e.g. a string representation of a number)

### Then we have the window function which generates sliding windows of a specified width from a long iterable. This one is pretty straight forward, it returns a list of sliding windows(substrings of the input string). One interesting part I did in this function is that I added a list called 'seen' which records every slinding windows(substrings) we have seen so we will not have repeated slinding windows(substrings) in the output list(non_repeated). This will reduce the run time complexity since a lof of redundant values will be checked later if the specified width is too small.

```python
def window(seq, width):
    '''
    this function takes an input string and returns all the substrings with the length wanted
    seq: str, input string to be sliced into 'windows'
    width: length of windows wanted
    returns: all the substrings(windows) with the length wanted(width)
    '''
    #exclude the number before the decimal
    seq=seq[1:]
    #create two lists, seen and non_repeated
    seen, non_repeated =[], []
    #iterate through the input string
    for i in range(0,len(seq)-(width-1)):
        #create windows of given width
        t = seq[i:i+width]
        #excluded repeated windows
        if t not in seen:
            #collect non-repeated windows
            non_repeated.append(t)
        #collect repeated windows as seen windows
        seen.append(t)
    #return a list of non-repeated windows
    return list(non_repeated)

```

```python
print(window(str(12345678), 4))
```

    ['2345', '3456', '4567', '5678']

### Unit test of window function:

```python
class TestNotebook(unittest.TestCase):

 def test_window(self):
    """test window."""
    self.assertEqual(window(str(12345678), 4), ['2345', '3456', '4567', '5678'])

unittest.main(argv=[''], verbosity=2, exit=False)
```

    test_window (__main__.TestNotebook)
    test window. ... ok

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK

    <unittest.main.TestProgram at 0x7f8427f0f710>

### -------------------------------------------------------------------------------------------------------
### Now use these helper functions to write the function that you need.
### Write a unit test for this final function, given that the first 10-digit prime in the expansion e is 7427466391.
### Finally, solve the given problem.

### This function uses all of the helper functions I wrote above.
### It iterates through the list of numbers generated by the window function from the string of the expansion of the given number generated by the create_expansion function, and then checks whether every number of this list is a prime number using the IsPrimeNumber function.
### Lastly, it returns the first prime number of the expansion with the wanted length of digits.

```python
def give_prime_expansion(digits_of_number, input_number, multiplier, length_of_prime):
    '''
    this function takes the digits of number, an input number('pi' or 'e'), a multiplier and the length of prime number we want, returns the first prime number in the expansion.
    digits_of_number: the number of decimal digits
    the input_number: pi or e
    multiplier: a multiplier of the input number
    length_of_prime: the length of the prime number we want
    returns: the first prime number in the decimal expansion of the input number
    '''
    #iterte through the list of windows of numbers
    for numbers in window(create_expansion(digits_of_number,input_number,multiplier),length_of_prime):
        #check if the number is prime
        if IsPrimeNumber(int(numbers)):
            #output the number we want
            print(f"The first {length_of_prime}-digit prime number in the decimal expansion of {multiplier} {input_number} is: ")
            return(int(numbers))
            break

```

### Thus we can find the first 10-digit prime in the decimal expansion of 17π.

```python
#example
print(give_prime_expansion(99,'pi',17,10))
```

    The first 10-digit prime number in the decimal expansion of 17 pi is:
    8649375157

### Unit test of give_prime_expansion function:

```python
class TestNotebook(unittest.TestCase):
    def test_give_prime_expansion(self):
        """test test_give_prime_expansion ."""
        self.assertEqual(give_prime_expansion(120,'e',1,10), 7427466391)
        self.assertEqual(give_prime_expansion(99,'pi',1,5), 14159)

unittest.main(argv=[''], verbosity=2, exit=False)
```

    test_give_prime_expansion (__main__.TestNotebook)
    test test_give_prime_expansion . ...

    The first 10-digit prime number in the decimal expansion of 1 e is:
    The first 5-digit prime number in the decimal expansion of 1 pi is:

    ok

    ----------------------------------------------------------------------
    Ran 1 test in 0.008s

    OK

    <unittest.main.TestProgram at 0x7f8427f19d90>

```python

```

```python

```


# Single Particle Model (SPM)

## Model Equations

The SPM consists of two spherically symmetric diffusion equations: one within a representative negative particle ($\text{k}=\text{n}$) and one within a representative positive particle ($\text{k}=\text{p}$). In the centre of the particle the standard no-flux condition is imposed. Since the SPM assumes that all particles in an electrode behave in exactly the same way, the flux on the surface of a particle is simply the current $I$ divided by the thickness of the electrode $L_{\text{k}}$. The concentration of lithium in electrode $\text{k}$ is denoted $c_{\text{k}}$ and the current is denoted by $I$. All parameters in the model stated here are dimensionless and are given in terms of dimensional parameters at the end of this notebook. The model equations for the SPM are then:
\begin{align}
\mathcal{C}_{\text{k}} \frac{\partial c_{\text{s,k}}}{\partial t} &= -\frac{1}{r_{\text{k}}^2} \frac{\partial}{\partial r_{\text{k}}} \left(r_{\text{k}}^2 N_{\text{s,k}}\right), \\
N_{\text{s,k}} &= -D_{\text{s,k}}(c_{\text{s,k}}) \frac{\partial c_{\text{s,k}}}{\partial r_{\text{k}}}, \quad \text{k} \in \text{n, p}, \end{align}
$$
N_{\text{s,k}}\big|_{r_{\text{k}}=0} = 0, \quad \text{k} \in \text{n, p}, \quad \ \ - \frac{a_{R, \text{k}}\gamma_{\text{k}}}{\mathcal{C}_{\text{k}}} N_{\text{s,k}}\big|_{r_{\text{k}}=1} =
\begin{cases}
		  \frac{I}{L_{\text{n}}}, \quad &\text{k}=\text{n}, \\
		  -\frac{I}{L_{\text{p}}}, \quad &\text{k}=\text{p},
\end{cases} \\
c_{\text{s,k}}(r_{\text{k}},0) = c_{\text{s,k,0}}, \quad \text{k} \in \text{n, p},$$
where $D_{\text{s,k}}$ is the diffusion coefficient in the solid, $N_{\text{s,k}}$ denotes the flux of lithium ions in the solid particle within the region $\text{k}$, and $r_{\text{k}} \in[0,1]$ is the radial coordinate of the particle in electrode $\text{k}$.

### Voltage Expression
The terminal voltage is obtained from the expression:
$$
V = U_{\text{p}}(c_{\text{p}})\big|_{r_{\text{p}}=1} - U_{\text{n}}(c_{\text{n}})\big|_{r_{\text{n}}=1} -2\sinh^{-1}\left(\frac{I}{j_{\text{0,p}} L_{\text{p}}}\right) - 2\sinh^{-1}\left(\frac{I}{j_{\text{0,n}} L_{\text{n}}}\right)
$$
with the exchange current densities given by
$$j_{\text{0,k}} =  \frac{\gamma_{\text{k}}}{\mathcal{C}_{\text{r,k}}}(c_{\text{k}})^{1/2}(1-c_{\text{k}})^{1/2}  $$

More details can be found in [[1]](#ref).

## Example solving SPM using PyBaMM

Below we show how to solve the Single Particle Model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM. In this notebook we explicitly handle all the stages of setting up, processing and solving the model in order to explain them in detail. However, it is often simpler in practice to use the `Simulation` class, which handles many of the stages automatically, as shown [here](../simulation-class.ipynb).

First we need to import `pybamm`, and then change our working directory to the root of the pybamm folder.

```python
%pip install pybamm -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt
os.chdir(pybamm.__path__[0]+'/..')
```

    Note: you may need to restart the kernel to use updated packages.

We then create an instance of the SPM:

```python
model = pybamm.lithium_ion.SPM()
```

The model object is a subtype of [`pybamm.BaseModel`](https://pybamm.readthedocs.io/en/latest/source/models/base_model.html), and contains all the equations that define this particular model. For example, the `rhs` dict contained in `model` has a dictionary mapping variables such as $c_n$ to the equation representing its rate of change with time (i.e. $\partial{c_n}/\partial{t}$). We can see this explicitly by visualising this entry in the `rhs` dict:

```python
variable = next(iter(model.rhs.keys()))
equation = next(iter(model.rhs.values()))
print('rhs equation for variable \'',variable,'\' is:')
path = 'examples/notebooks/models/'
equation.visualise(path+'spm1.png')
```

    rhs equation for variable ' Discharge capacity [A.h] ' is:

We need a geometry in which to define our model equations. In pybamm this is represented by the [`pybamm.Geometry`](https://pybamm.readthedocs.io/en/latest/source/geometry/geometry.html) class. In this case we use the default geometry object defined by the model

```python
geometry = model.default_geometry
```

This geometry object defines a number of domains, each with its own name, spatial variables and min/max limits (the latter are represented as equations similar to the rhs equation shown above). For instance, the SPM has the following domains:

```python
print('SPM domains:')
for i, (k, v) in enumerate(geometry.items()):
    print(str(i+1)+'.',k,'with variables:')
    for var, rng in v.items():
        if 'min' in rng:
            print('  -(',rng['min'],') <=',var,'<= (',rng['max'],')')
        else:
            print(var, '=', rng['position'])
```

    SPM domains:
    1. negative electrode with variables:
      -( 0 ) <= x_n <= ( Negative electrode thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) )
    2. separator with variables:
      -( Negative electrode thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) ) <= x_s <= ( Negative electrode thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) + Separator thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) )
    3. positive electrode with variables:
      -( Negative electrode thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) + Separator thickness [m] / (Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]) ) <= x_p <= ( 1 )
    4. negative particle with variables:
      -( 0 ) <= r_n <= ( 1 )
    5. positive particle with variables:
      -( 0 ) <= r_p <= ( 1 )
    6. current collector with variables:
    z = 1

Both the model equations and the geometry include parameters, such as $\gamma_p$ or $L_p$. We can substitute these symbolic parameters in the model with values by using the [`pybamm.ParameterValues`](https://pybamm.readthedocs.io/en/latest/source/parameters/parameter_values.html) class, which takes either a python dictionary or CSV file with the mapping between parameter names and values. Rather than create our own instance of `pybamm.ParameterValues`, we will use the default parameter set included in the model

```python
param = model.default_parameter_values
```

We can then apply this parameter set to the model and geometry

```python
param.process_model(model)
param.process_geometry(geometry)
```

The next step is to mesh the input geometry. We can do this using the [`pybamm.Mesh`](https://pybamm.readthedocs.io/en/latest/source/meshes/meshes.html) class. This class takes in the geometry of the problem, and also two dictionaries containing the type of mesh to use within each domain of the geometry (i.e. within the positive or negative electrode domains), and the number of mesh points.

The default mesh types and the default number of points to use in each variable for the SPM are:

```python
for k, t in model.default_submesh_types.items():
    print(k,'is of type',t.__repr__())
for var, npts in model.default_var_pts.items():
    print(var,'has',npts,'mesh points')
```

    negative electrode is of type Generator for Uniform1DSubMesh
    separator is of type Generator for Uniform1DSubMesh
    positive electrode is of type Generator for Uniform1DSubMesh
    negative particle is of type Generator for Uniform1DSubMesh
    positive particle is of type Generator for Uniform1DSubMesh
    current collector is of type Generator for SubMesh0D
    x_n has 20 mesh points
    x_s has 20 mesh points
    x_p has 20 mesh points
    r_n has 30 mesh points
    r_p has 30 mesh points
    y has 10 mesh points
    z has 10 mesh points

With these defaults, we can then create our mesh of the given geometry:

```python
mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)
```

The next step is to discretise the model equations using this mesh. We do this using the [`pybamm.Discretisation`](https://pybamm.readthedocs.io/en/latest/source/discretisations/discretisation.html) class, which takes both the mesh we have already created, and a dictionary of spatial methods to use for each geometry domain. For the case of the SPM, we use the following defaults for the spatial discretisation methods:

```python
for k, method in model.default_spatial_methods.items():
    print(k,'is discretised using',method.__class__.__name__,'method')
```

    macroscale is discretised using FiniteVolume method
    negative particle is discretised using FiniteVolume method
    positive particle is discretised using FiniteVolume method
    current collector is discretised using ZeroDimensionalSpatialMethod method

We then create the `pybamm.Discretisation` object, and use this to discretise the model equations

```python
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model);
```

After this stage, all of the variables in `model` have been discretised into `pybamm.StateVector` objects, and spatial operators have been replaced by matrix-vector multiplications, ready to be evaluated within a time-stepping algorithm of a given solver. For example, the rhs expression for $\partial{c_n}/\partial{t}$ that we visualised above is now represented by:

```python
model.concatenated_rhs.children[0].visualise(path+'spm2.png')
```

Now we are ready to run the time-stepping routine to solve the model. Once again we use the default ODE solver.

```python
# Solve the model at the given time points (in seconds)
solver = model.default_solver
n = 250
t_eval = np.linspace(0, 3600, n)
print('Solving using',type(solver).__name__,'solver...')
solution = solver.solve(model, t_eval)
print('Finished.')
```

    Solving using ScipySolver solver...
    Finished.

Each model in pybamm has a list of relevant variables defined in the model, for use in visualising the model solution or for comparison with other models. The SPM defines the following variables:

```python
print('SPM model variables:')
for v in model.variables.keys():
    print('\t-',v)
```

    SPM model variables:
    	- Time
    	- Time [s]
    	- Time [min]
    	- Time [h]
    	- x
    	- x [m]
    	- x_n
    	- x_n [m]
    	- x_s
    	- x_s [m]
    	- x_p
    	- x_p [m]
    	- Sum of electrolyte reaction source terms
    	- Sum of negative electrode electrolyte reaction source terms
    	- Sum of positive electrode electrolyte reaction source terms
    	- Sum of x-averaged negative electrode electrolyte reaction source terms
    	- Sum of x-averaged positive electrode electrolyte reaction source terms
    	- Sum of interfacial current densities
    	- Sum of negative electrode interfacial current densities
    	- Sum of positive electrode interfacial current densities
    	- Sum of x-averaged negative electrode interfacial current densities
    	- Sum of x-averaged positive electrode interfacial current densities
    	- r_n
    	- r_n [m]
    	- r_p
    	- r_p [m]
    	- Total current density
    	- Total current density [A.m-2]
    	- Current [A]
    	- C-rate
    	- Discharge capacity [A.h]
    	- Porosity
    	- Negative electrode porosity
    	- Separator porosity
    	- Positive electrode porosity
    	- X-averaged negative electrode porosity
    	- X-averaged separator porosity
    	- X-averaged positive electrode porosity
    	- Active material volume fraction
    	- Negative electrode active material volume fraction
    	- Separator active material volume fraction
    	- Positive electrode active material volume fraction
    	- X-averaged negative electrode active material volume fraction
    	- X-averaged separator active material volume fraction
    	- X-averaged positive electrode active material volume fraction
    	- Leading-order porosity
    	- Leading-order negative electrode porosity
    	- Leading-order separator porosity
    	- Leading-order positive electrode porosity
    	- Leading-order x-averaged negative electrode porosity
    	- Leading-order x-averaged separator porosity
    	- Leading-order x-averaged positive electrode porosity
    	- Leading-order active material volume fraction
    	- Leading-order negative electrode active material volume fraction
    	- Leading-order separator active material volume fraction
    	- Leading-order positive electrode active material volume fraction
    	- Leading-order x-averaged negative electrode active material volume fraction
    	- Leading-order x-averaged separator active material volume fraction
    	- Leading-order x-averaged positive electrode active material volume fraction
    	- Porosity change
    	- Negative electrode porosity change
    	- Separator porosity change
    	- Positive electrode porosity change
    	- X-averaged negative electrode porosity change
    	- X-averaged separator porosity change
    	- X-averaged positive electrode porosity change
    	- Leading-order x-averaged negative electrode porosity change
    	- Leading-order x-averaged separator porosity change
    	- Leading-order x-averaged positive electrode porosity change
    	- Negative electrode volume-averaged velocity
    	- Positive electrode volume-averaged velocity
    	- Negative electrode volume-averaged velocity [m.s-1]
    	- Positive electrode volume-averaged velocity [m.s-1]
    	- Negative electrode volume-averaged acceleration
    	- Positive electrode volume-averaged acceleration
    	- Negative electrode volume-averaged acceleration [m.s-1]
    	- Positive electrode volume-averaged acceleration [m.s-1]
    	- X-averaged negative electrode volume-averaged acceleration
    	- X-averaged positive electrode volume-averaged acceleration
    	- X-averaged negative electrode volume-averaged acceleration [m.s-1]
    	- X-averaged positive electrode volume-averaged acceleration [m.s-1]
    	- Negative electrode pressure
    	- Positive electrode pressure
    	- X-averaged negative electrode pressure
    	- X-averaged positive electrode pressure
    	- Separator pressure
    	- X-averaged separator pressure
    	- Negative electrode transverse volume-averaged velocity
    	- Separator transverse volume-averaged velocity
    	- Positive electrode transverse volume-averaged velocity
    	- Negative electrode transverse volume-averaged velocity [m.s-2]
    	- Separator transverse volume-averaged velocity [m.s-2]
    	- Positive electrode transverse volume-averaged velocity [m.s-2]
    	- X-averaged negative electrode transverse volume-averaged velocity
    	- X-averaged separator transverse volume-averaged velocity
    	- X-averaged positive electrode transverse volume-averaged velocity
    	- X-averaged negative electrode transverse volume-averaged velocity [m.s-2]
    	- X-averaged separator transverse volume-averaged velocity [m.s-2]
    	- X-averaged positive electrode transverse volume-averaged velocity [m.s-2]
    	- Transverse volume-averaged velocity
    	- Transverse volume-averaged velocity [m.s-2]
    	- Negative electrode transverse volume-averaged acceleration
    	- Separator transverse volume-averaged acceleration
    	- Positive electrode transverse volume-averaged acceleration
    	- Negative electrode transverse volume-averaged acceleration [m.s-2]
    	- Separator transverse volume-averaged acceleration [m.s-2]
    	- Positive electrode transverse volume-averaged acceleration [m.s-2]
    	- X-averaged negative electrode transverse volume-averaged acceleration
    	- X-averaged separator transverse volume-averaged acceleration
    	- X-averaged positive electrode transverse volume-averaged acceleration
    	- X-averaged negative electrode transverse volume-averaged acceleration [m.s-2]
    	- X-averaged separator transverse volume-averaged acceleration [m.s-2]
    	- X-averaged positive electrode transverse volume-averaged acceleration [m.s-2]
    	- Transverse volume-averaged acceleration
    	- Transverse volume-averaged acceleration [m.s-2]
    	- Negative particle concentration
    	- Negative particle concentration [mol.m-3]
    	- X-averaged negative particle concentration
    	- X-averaged negative particle concentration [mol.m-3]
    	- R-averaged negative particle concentration
    	- R-averaged negative particle concentration [mol.m-3]
    	- Average negative particle concentration
    	- Average negative particle concentration [mol.m-3]
    	- Negative particle surface concentration
    	- Negative particle surface concentration [mol.m-3]
    	- X-averaged negative particle surface concentration
    	- X-averaged negative particle surface concentration [mol.m-3]
    	- Negative electrode active volume fraction
    	- Negative electrode volume-averaged concentration
    	- Negative electrode volume-averaged concentration [mol.m-3]
    	- Negative electrode extent of lithiation
    	- X-averaged negative electrode extent of lithiation
    	- Total lithium in negative electrode [mol]
    	- Minimum negative particle concentration
    	- Maximum negative particle concentration
    	- Minimum negative particle concentration [mol.m-3]
    	- Maximum negative particle concentration [mol.m-3]
    	- Minimum negative particle surface concentration
    	- Maximum negative particle surface concentration
    	- Minimum negative particle surface concentration [mol.m-3]
    	- Maximum negative particle surface concentration [mol.m-3]
    	- Negative particle radius
    	- Negative particle radius [m]
    	- Negative surface area to volume ratio
    	- Negative surface area to volume ratio [m-1]
    	- Positive particle concentration
    	- Positive particle concentration [mol.m-3]
    	- X-averaged positive particle concentration
    	- X-averaged positive particle concentration [mol.m-3]
    	- R-averaged positive particle concentration
    	- R-averaged positive particle concentration [mol.m-3]
    	- Average positive particle concentration
    	- Average positive particle concentration [mol.m-3]
    	- Positive particle surface concentration
    	- Positive particle surface concentration [mol.m-3]
    	- X-averaged positive particle surface concentration
    	- X-averaged positive particle surface concentration [mol.m-3]
    	- Positive electrode active volume fraction
    	- Positive electrode volume-averaged concentration
    	- Positive electrode volume-averaged concentration [mol.m-3]
    	- Positive electrode extent of lithiation
    	- X-averaged positive electrode extent of lithiation
    	- Total lithium in positive electrode [mol]
    	- Minimum positive particle concentration
    	- Maximum positive particle concentration
    	- Minimum positive particle concentration [mol.m-3]
    	- Maximum positive particle concentration [mol.m-3]
    	- Minimum positive particle surface concentration
    	- Maximum positive particle surface concentration
    	- Minimum positive particle surface concentration [mol.m-3]
    	- Maximum positive particle surface concentration [mol.m-3]
    	- Positive particle radius
    	- Positive particle radius [m]
    	- Positive surface area to volume ratio
    	- Positive surface area to volume ratio [m-1]
    	- Electrolyte concentration
    	- Electrolyte concentration [mol.m-3]
    	- Electrolyte concentration [Molar]
    	- X-averaged electrolyte concentration
    	- X-averaged electrolyte concentration [mol.m-3]
    	- X-averaged electrolyte concentration [Molar]
    	- Negative electrolyte concentration
    	- Negative electrolyte concentration [mol.m-3]
    	- Negative electrolyte concentration [Molar]
    	- Separator electrolyte concentration
    	- Separator electrolyte concentration [mol.m-3]
    	- Separator electrolyte concentration [Molar]
    	- Positive electrolyte concentration
    	- Positive electrolyte concentration [mol.m-3]
    	- Positive electrolyte concentration [Molar]
    	- X-averaged negative electrolyte concentration
    	- X-averaged negative electrolyte concentration [mol.m-3]
    	- X-averaged separator electrolyte concentration
    	- X-averaged separator electrolyte concentration [mol.m-3]
    	- X-averaged positive electrolyte concentration
    	- X-averaged positive electrolyte concentration [mol.m-3]
    	- Electrolyte flux
    	- Electrolyte flux [mol.m-2.s-1]
    	- Negative current collector temperature
    	- Negative current collector temperature [K]
    	- X-averaged negative electrode temperature
    	- X-averaged negative electrode temperature [K]
    	- Negative electrode temperature
    	- Negative electrode temperature [K]
    	- X-averaged separator temperature
    	- X-averaged separator temperature [K]
    	- Separator temperature
    	- Separator temperature [K]
    	- X-averaged positive electrode temperature
    	- X-averaged positive electrode temperature [K]
    	- Positive electrode temperature
    	- Positive electrode temperature [K]
    	- Positive current collector temperature
    	- Positive current collector temperature [K]
    	- Cell temperature
    	- Cell temperature [K]
    	- X-averaged cell temperature
    	- X-averaged cell temperature [K]
    	- Volume-averaged cell temperature
    	- Volume-averaged cell temperature [K]
    	- Ambient temperature [K]
    	- Ambient temperature
    	- Inner negative electrode sei thickness
    	- Inner negative electrode sei thickness [m]
    	- X-averaged inner negative electrode sei thickness
    	- X-averaged inner negative electrode sei thickness [m]
    	- Outer negative electrode sei thickness
    	- Outer negative electrode sei thickness [m]
    	- X-averaged outer negative electrode sei thickness
    	- X-averaged outer negative electrode sei thickness [m]
    	- Total negative electrode sei thickness
    	- Total negative electrode sei thickness [m]
    	- X-averaged total negative electrode sei thickness
    	- X-averaged total negative electrode sei thickness [m]
    	- X-averaged negative electrode resistance [Ohm.m2]
    	- Inner negative electrode sei concentration [mol.m-3]
    	- X-averaged inner negative electrode sei concentration [mol.m-3]
    	- Outer negative electrode sei concentration [mol.m-3]
    	- X-averaged outer negative electrode sei concentration [mol.m-3]
    	- Negative sei concentration [mol.m-3]
    	- X-averaged negative electrode sei concentration [mol.m-3]
    	- Loss of lithium to negative electrode sei [mol]
    	- Total negative electrode sei thickness
    	- Total negative electrode sei thickness [m]
    	- X-averaged total negative electrode sei thickness
    	- X-averaged total negative electrode sei thickness [m]
    	- X-averaged negative electrode resistance [Ohm.m2]
    	- Inner negative electrode sei interfacial current density
    	- Inner negative electrode sei interfacial current density [A.m-2]
    	- X-averaged inner negative electrode sei interfacial current density
    	- X-averaged inner negative electrode sei interfacial current density [A.m-2]
    	- Outer negative electrode sei interfacial current density
    	- Outer negative electrode sei interfacial current density [A.m-2]
    	- X-averaged outer negative electrode sei interfacial current density
    	- X-averaged outer negative electrode sei interfacial current density [A.m-2]
    	- Negative electrode sei interfacial current density
    	- Negative electrode sei interfacial current density [A.m-2]
    	- X-averaged negative electrode sei interfacial current density
    	- X-averaged negative electrode sei interfacial current density [A.m-2]
    	- Inner positive electrode sei thickness
    	- Inner positive electrode sei thickness [m]
    	- X-averaged inner positive electrode sei thickness
    	- X-averaged inner positive electrode sei thickness [m]
    	- Outer positive electrode sei thickness
    	- Outer positive electrode sei thickness [m]
    	- X-averaged outer positive electrode sei thickness
    	- X-averaged outer positive electrode sei thickness [m]
    	- Total positive electrode sei thickness
    	- Total positive electrode sei thickness [m]
    	- X-averaged total positive electrode sei thickness
    	- X-averaged total positive electrode sei thickness [m]
    	- X-averaged positive electrode resistance [Ohm.m2]
    	- Inner positive electrode sei concentration [mol.m-3]
    	- X-averaged inner positive electrode sei concentration [mol.m-3]
    	- Outer positive electrode sei concentration [mol.m-3]
    	- X-averaged outer positive electrode sei concentration [mol.m-3]
    	- Positive sei concentration [mol.m-3]
    	- X-averaged positive electrode sei concentration [mol.m-3]
    	- Loss of lithium to positive electrode sei [mol]
    	- Total positive electrode sei thickness
    	- Total positive electrode sei thickness [m]
    	- X-averaged total positive electrode sei thickness
    	- X-averaged total positive electrode sei thickness [m]
    	- X-averaged positive electrode resistance [Ohm.m2]
    	- Inner positive electrode sei interfacial current density
    	- Inner positive electrode sei interfacial current density [A.m-2]
    	- X-averaged inner positive electrode sei interfacial current density
    	- X-averaged inner positive electrode sei interfacial current density [A.m-2]
    	- Outer positive electrode sei interfacial current density
    	- Outer positive electrode sei interfacial current density [A.m-2]
    	- X-averaged outer positive electrode sei interfacial current density
    	- X-averaged outer positive electrode sei interfacial current density [A.m-2]
    	- Positive electrode sei interfacial current density
    	- Positive electrode sei interfacial current density [A.m-2]
    	- X-averaged positive electrode sei interfacial current density
    	- X-averaged positive electrode sei interfacial current density [A.m-2]
    	- Electrolyte tortuosity
    	- Negative electrolyte tortuosity
    	- Positive electrolyte tortuosity
    	- X-averaged negative electrolyte tortuosity
    	- X-averaged positive electrolyte tortuosity
    	- Separator tortuosity
    	- X-averaged separator tortuosity
    	- Electrode tortuosity
    	- Negative electrode tortuosity
    	- Positive electrode tortuosity
    	- X-averaged negative electrode tortuosity
    	- X-averaged positive electrode tortuosity
    	- Separator volume-averaged velocity
    	- Separator volume-averaged velocity [m.s-1]
    	- Separator volume-averaged acceleration
    	- Separator volume-averaged acceleration [m.s-1]
    	- X-averaged separator volume-averaged acceleration
    	- X-averaged separator volume-averaged acceleration [m.s-1]
    	- Volume-averaged velocity
    	- Volume-averaged velocity [m.s-1]
    	- Volume-averaged acceleration
    	- X-averaged volume-averaged acceleration
    	- Volume-averaged acceleration [m.s-1]
    	- X-averaged volume-averaged acceleration [m.s-1]
    	- Pressure
    	- Negative particle flux
    	- X-averaged negative particle flux
    	- Positive particle flux
    	- X-averaged positive particle flux
    	- Total concentration in electrolyte [mol]
    	- Ohmic heating
    	- Ohmic heating [W.m-3]
    	- X-averaged Ohmic heating
    	- X-averaged Ohmic heating [W.m-3]
    	- Volume-averaged Ohmic heating
    	- Volume-averaged Ohmic heating [W.m-3]
    	- Irreversible electrochemical heating
    	- Irreversible electrochemical heating [W.m-3]
    	- X-averaged irreversible electrochemical heating
    	- X-averaged irreversible electrochemical heating [W.m-3]
    	- Volume-averaged irreversible electrochemical heating
    	- Volume-averaged irreversible electrochemical heating[W.m-3]
    	- Reversible heating
    	- Reversible heating [W.m-3]
    	- X-averaged reversible heating
    	- X-averaged reversible heating [W.m-3]
    	- Volume-averaged reversible heating
    	- Volume-averaged reversible heating [W.m-3]
    	- Total heating
    	- Total heating [W.m-3]
    	- X-averaged total heating
    	- X-averaged total heating [W.m-3]
    	- Volume-averaged total heating
    	- Volume-averaged total heating [W.m-3]
    	- Negative current collector potential
    	- Negative current collector potential [V]
    	- Current collector current density
    	- Current collector current density [A.m-2]
    	- Leading-order current collector current density
    	- Sei interfacial current density
    	- Sei interfacial current density [A.m-2]
    	- Sei interfacial current density per volume [A.m-3]
    	- X-averaged negative electrode total interfacial current density
    	- X-averaged negative electrode total interfacial current density [A.m-2]
    	- X-averaged negative electrode total interfacial current density per volume [A.m-3]
    	- Negative electrode exchange current density
    	- X-averaged negative electrode exchange current density
    	- Negative electrode exchange current density [A.m-2]
    	- X-averaged negative electrode exchange current density [A.m-2]
    	- Negative electrode exchange current density per volume [A.m-3]
    	- X-averaged negative electrode exchange current density per volume [A.m-3]
    	- Negative electrode reaction overpotential
    	- X-averaged negative electrode reaction overpotential
    	- Negative electrode reaction overpotential [V]
    	- X-averaged negative electrode reaction overpotential [V]
    	- Negative electrode surface potential difference
    	- X-averaged negative electrode surface potential difference
    	- Negative electrode surface potential difference [V]
    	- X-averaged negative electrode surface potential difference [V]
    	- Negative electrode sei film overpotential
    	- X-averaged negative electrode sei film overpotential
    	- Negative electrode sei film overpotential [V]
    	- X-averaged negative electrode sei film overpotential [V]
    	- Negative electrode open circuit potential
    	- Negative electrode open circuit potential [V]
    	- X-averaged negative electrode open circuit potential
    	- X-averaged negative electrode open circuit potential [V]
    	- Negative electrode entropic change
    	- X-averaged negative electrode entropic change
    	- X-averaged positive electrode total interfacial current density
    	- X-averaged positive electrode total interfacial current density [A.m-2]
    	- X-averaged positive electrode total interfacial current density per volume [A.m-3]
    	- Positive electrode exchange current density
    	- X-averaged positive electrode exchange current density
    	- Positive electrode exchange current density [A.m-2]
    	- X-averaged positive electrode exchange current density [A.m-2]
    	- Positive electrode exchange current density per volume [A.m-3]
    	- X-averaged positive electrode exchange current density per volume [A.m-3]
    	- Positive electrode reaction overpotential
    	- X-averaged positive electrode reaction overpotential
    	- Positive electrode reaction overpotential [V]
    	- X-averaged positive electrode reaction overpotential [V]
    	- Positive electrode surface potential difference
    	- X-averaged positive electrode surface potential difference
    	- Positive electrode surface potential difference [V]
    	- X-averaged positive electrode surface potential difference [V]
    	- Positive electrode sei film overpotential
    	- X-averaged positive electrode sei film overpotential
    	- Positive electrode sei film overpotential [V]
    	- X-averaged positive electrode sei film overpotential [V]
    	- Positive electrode open circuit potential
    	- Positive electrode open circuit potential [V]
    	- X-averaged positive electrode open circuit potential
    	- X-averaged positive electrode open circuit potential [V]
    	- Positive electrode entropic change
    	- X-averaged positive electrode entropic change
    	- Negative electrode interfacial current density
    	- X-averaged negative electrode interfacial current density
    	- Negative electrode interfacial current density [A.m-2]
    	- X-averaged negative electrode interfacial current density [A.m-2]
    	- Negative electrode interfacial current density per volume [A.m-3]
    	- X-averaged negative electrode interfacial current density per volume [A.m-3]
    	- Positive electrode interfacial current density
    	- X-averaged positive electrode interfacial current density
    	- Positive electrode interfacial current density [A.m-2]
    	- X-averaged positive electrode interfacial current density [A.m-2]
    	- Positive electrode interfacial current density per volume [A.m-3]
    	- X-averaged positive electrode interfacial current density per volume [A.m-3]
    	- Interfacial current density
    	- Interfacial current density [A.m-2]
    	- Interfacial current density per volume [A.m-3]
    	- Exchange current density
    	- Exchange current density [A.m-2]
    	- Exchange current density per volume [A.m-3]
    	- Negative electrode oxygen interfacial current density
    	- X-averaged negative electrode oxygen interfacial current density
    	- Negative electrode oxygen interfacial current density [A.m-2]
    	- X-averaged negative electrode oxygen interfacial current density [A.m-2]
    	- Negative electrode oxygen interfacial current density per volume [A.m-3]
    	- X-averaged negative electrode oxygen interfacial current density per volume [A.m-3]
    	- Negative electrode oxygen exchange current density
    	- X-averaged negative electrode oxygen exchange current density
    	- Negative electrode oxygen exchange current density [A.m-2]
    	- X-averaged negative electrode oxygen exchange current density [A.m-2]
    	- Negative electrode oxygen exchange current density per volume [A.m-3]
    	- X-averaged negative electrode oxygen exchange current density per volume [A.m-3]
    	- Negative electrode oxygen reaction overpotential
    	- X-averaged negative electrode oxygen reaction overpotential
    	- Negative electrode oxygen reaction overpotential [V]
    	- X-averaged negative electrode oxygen reaction overpotential [V]
    	- Negative electrode oxygen open circuit potential
    	- Negative electrode oxygen open circuit potential [V]
    	- X-averaged negative electrode oxygen open circuit potential
    	- X-averaged negative electrode oxygen open circuit potential [V]
    	- Positive electrode oxygen interfacial current density
    	- X-averaged positive electrode oxygen interfacial current density
    	- Positive electrode oxygen interfacial current density [A.m-2]
    	- X-averaged positive electrode oxygen interfacial current density [A.m-2]
    	- Positive electrode oxygen interfacial current density per volume [A.m-3]
    	- X-averaged positive electrode oxygen interfacial current density per volume [A.m-3]
    	- Positive electrode oxygen exchange current density
    	- X-averaged positive electrode oxygen exchange current density
    	- Positive electrode oxygen exchange current density [A.m-2]
    	- X-averaged positive electrode oxygen exchange current density [A.m-2]
    	- Positive electrode oxygen exchange current density per volume [A.m-3]
    	- X-averaged positive electrode oxygen exchange current density per volume [A.m-3]
    	- Positive electrode oxygen reaction overpotential
    	- X-averaged positive electrode oxygen reaction overpotential
    	- Positive electrode oxygen reaction overpotential [V]
    	- X-averaged positive electrode oxygen reaction overpotential [V]
    	- Positive electrode oxygen open circuit potential
    	- Positive electrode oxygen open circuit potential [V]
    	- X-averaged positive electrode oxygen open circuit potential
    	- X-averaged positive electrode oxygen open circuit potential [V]
    	- Oxygen interfacial current density
    	- Oxygen interfacial current density [A.m-2]
    	- Oxygen interfacial current density per volume [A.m-3]
    	- Oxygen exchange current density
    	- Oxygen exchange current density [A.m-2]
    	- Oxygen exchange current density per volume [A.m-3]
    	- Negative electrode potential
    	- Negative electrode potential [V]
    	- X-averaged negative electrode potential
    	- X-averaged negative electrode potential [V]
    	- Negative electrode ohmic losses
    	- Negative electrode ohmic losses [V]
    	- X-averaged negative electrode ohmic losses
    	- X-averaged negative electrode ohmic losses [V]
    	- Gradient of negative electrode potential
    	- Negative electrode current density
    	- Negative electrode current density [A.m-2]
    	- Negative electrolyte potential
    	- Negative electrolyte potential [V]
    	- Separator electrolyte potential
    	- Separator electrolyte potential [V]
    	- Positive electrolyte potential
    	- Positive electrolyte potential [V]
    	- Electrolyte potential
    	- Electrolyte potential [V]
    	- X-averaged electrolyte potential
    	- X-averaged electrolyte potential [V]
    	- X-averaged negative electrolyte potential
    	- X-averaged negative electrolyte potential [V]
    	- X-averaged separator electrolyte potential
    	- X-averaged separator electrolyte potential [V]
    	- X-averaged positive electrolyte potential
    	- X-averaged positive electrolyte potential [V]
    	- X-averaged electrolyte overpotential
    	- X-averaged electrolyte overpotential [V]
    	- Gradient of negative electrolyte potential
    	- Gradient of separator electrolyte potential
    	- Gradient of positive electrolyte potential
    	- Gradient of electrolyte potential
    	- Electrolyte current density
    	- Electrolyte current density [A.m-2]
    	- Negative electrolyte current density
    	- Negative electrolyte current density [A.m-2]
    	- Positive electrolyte current density
    	- Positive electrolyte current density [A.m-2]
    	- X-averaged concentration overpotential
    	- X-averaged electrolyte ohmic losses
    	- X-averaged concentration overpotential [V]
    	- X-averaged electrolyte ohmic losses [V]
    	- Positive electrode potential
    	- Positive electrode potential [V]
    	- X-averaged positive electrode potential
    	- X-averaged positive electrode potential [V]
    	- Positive electrode ohmic losses
    	- Positive electrode ohmic losses [V]
    	- X-averaged positive electrode ohmic losses
    	- X-averaged positive electrode ohmic losses [V]
    	- Gradient of positive electrode potential
    	- Positive electrode current density
    	- Positive electrode current density [A.m-2]
    	- Electrode current density
    	- Positive current collector potential
    	- Positive current collector potential [V]
    	- Local voltage
    	- Local voltage [V]
    	- Terminal voltage
    	- Terminal voltage [V]
    	- X-averaged open circuit voltage
    	- Measured open circuit voltage
    	- X-averaged open circuit voltage [V]
    	- Measured open circuit voltage [V]
    	- X-averaged reaction overpotential
    	- X-averaged reaction overpotential [V]
    	- X-averaged sei film overpotential
    	- X-averaged sei film overpotential [V]
    	- X-averaged solid phase ohmic losses
    	- X-averaged solid phase ohmic losses [V]
    	- X-averaged battery open circuit voltage [V]
    	- Measured battery open circuit voltage [V]
    	- X-averaged battery reaction overpotential [V]
    	- X-averaged battery solid phase ohmic losses [V]
    	- X-averaged battery electrolyte ohmic losses [V]
    	- X-averaged battery concentration overpotential [V]
    	- Battery voltage [V]
    	- Change in measured open circuit voltage
    	- Change in measured open circuit voltage [V]
    	- Local ECM resistance
    	- Local ECM resistance [Ohm]
    	- Terminal power [W]

To help visualise the results, pybamm provides the `pybamm.ProcessedVariable` class, which takes the output of a solver and a variable, and allows the user to evaluate the value of that variable at any given time or $x$ value. These processed variables are automatically created by the solution dictionary.

```python
voltage = solution['Terminal voltage [V]']
c_s_n_surf = solution['Negative particle surface concentration']
c_s_p_surf = solution['Positive particle surface concentration']
```

One we have these variables in hand, we can begin generating plots using a library such as Matplotlib. Below we plot the terminal voltage and surface particle concentrations versus time

```python
t = solution["Time [s]"].entries
x = solution["x [m]"].entries[:, 0]
f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13,4))

ax1.plot(t, voltage(t))
ax1.set_xlabel(r'$Time [s]$')
ax1.set_ylabel('Terminal voltage [V]')

ax2.plot(t, c_s_n_surf(t=t, x=x[0]))  # can evaluate at arbitrary x (single representative particle)
ax2.set_xlabel(r'$Time [s]$')
ax2.set_ylabel('Negative particle surface concentration')

ax3.plot(t, c_s_p_surf(t=t, x=x[-1]))  # can evaluate at arbitrary x (single representative particle)
ax3.set_xlabel(r'$Time [s]$')
ax3.set_ylabel('Positive particle surface concentration')

plt.tight_layout()
plt.show()
```

Some of the output variables are defined over space as well as time. Once option to visualise these variables is to use the `interact` slider widget. Below we plot the negative/positive particle concentration over $r$, using a slider to change the current time point

```python
c_s_n = solution['Negative particle concentration']
c_s_p = solution['Positive particle concentration']
r_n = solution["r_n [m]"].entries[:, 0]
r_p = solution["r_p [m]"].entries[:, 0]
```

```python
c_s_n = solution['Negative particle concentration']
c_s_p = solution['Positive particle concentration']
r_n = solution["r_n [m]"].entries[:, 0, 0]
r_p = solution["r_p [m]"].entries[:, 0, 0]

def plot_concentrations(t):
    f, (ax1, ax2) = plt.subplots(1, 2 ,figsize=(10,5))
    plot_c_n, = ax1.plot(r_n, c_s_n(r=r_n,t=t,x=x[0]))  # can evaluate at arbitrary x (single representative particle)
    plot_c_p, = ax2.plot(r_p, c_s_p(r=r_p,t=t,x=x[-1]))  # can evaluate at arbitrary x (single representative particle)
    ax1.set_ylabel('Negative particle concentration')
    ax2.set_ylabel('Positive particle concentration')
    ax1.set_xlabel(r'$r_n$ [m]')
    ax2.set_xlabel(r'$r_p$ [m]')
    ax1.set_ylim(0, 1)
    ax2.set_ylim(0, 1)
    plt.show()

import ipywidgets as widgets
widgets.interact(plot_concentrations, t=widgets.FloatSlider(min=0,max=3600,step=10,value=0));

```

    interactive(children=(FloatSlider(value=0.0, description='t', max=3600.0, step=10.0), Output()), _dom_classes=…

The QuickPlot class can be used to plot the common set of useful outputs which should give you a good initial overview of the model. The method `Quickplot.dynamic_plot` employs the slider widget.

```python
quick_plot = pybamm.QuickPlot(solution)
quick_plot.dynamic_plot();
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…

## Dimensionless Parameters

In the table below, we provide the dimensionless parameters in the SPM in terms of the dimensional parameters in LCO.csv. We use a superscript * to indicate dimensional quantities.

| Parameter                 | Expression                              |Interpretation                             |
|:--------------------------|:----------------------------------------|:------------------------------------------|
| $L_{\text{k}}$            | $L_{\text{k}}^*/L^*$                    | Ratio of region thickness to cell thickness|
|$\mathcal{C}_{\text{k}}$   | $\tau_{\text{k}}^*/\tau_{\text{d}}^*$   | Ratio of solid diffusion and discharge timescales |
|$\mathcal{C}_{\text{r,k}}$ |$\tau_{\text{r,k}}^*/\tau_{\text{d}}^*$  |Ratio of reaction and discharge timescales|
|$a_{R, \text{k}}$             |$a_{\text{k}}^* R_{\text{k}}^*$          | Product of particle radius and surface area to volume ratio|
|$\gamma_{\text{k}}$        |$c_{\text{k,max}}^*/c_{\text{n,max}}^*$  |Ratio of maximum lithium concentrations in solid|

<a name="ref">[1]</a> Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. "An asymptotic derivation of a single particle model with electrolyte." Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019

```python

```